All skills
apollographql avatar

/apollo-client

@cb48e40 official
by Apollo GraphQLapollographql/skills115 stars
13

Guide for building React applications with Apollo Client 4.x. Use this skill when: (1) setting up Apollo Client in a React project, (2) writing GraphQL queries or mutations with hooks, (3) configuring caching or cache policies, (4) managing local state with reactive variables, (5) troubleshooting Apollo Client errors or performance issues.

Use this Skill: https://skilld.dev/gh/apollographql/skills/apollo-client

This session only. Nothing lands on disk.

referencesmutations.md

≈3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Mutations Reference

Table of Contents

useMutation Hook

The useMutation hook is used to execute GraphQL mutations.

Basic Usage

import { gql } from "@apollo/client";
import { useMutation } from "@apollo/client/react";

const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

function AddTodo() {
  const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const form = e.currentTarget;
        const text = new FormData(form).get("text") as string;
        addTodo({ variables: { text } });
        form.reset();
      }}
    >
      <input name="text" placeholder="Add todo" />
      <button type="submit" disabled={loading}>
        Add
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Return Tuple

const [
  mutateFunction, // Function to call to execute mutation
  {
    data, // Mutation result data
    loading, // True while mutation is in flight
    error, // ApolloError if mutation failed
    called, // True if mutation has been called
    reset, // Reset mutation state
    client, // Apollo Client instance
  },
] = useMutation(MUTATION);

Mutation Variables

Variables in Options

const [createUser] = useMutation(CREATE_USER, {
  variables: {
    input: {
      name: "Default User",
      email: "default@example.com",
    },
  },
});

// Call with default variables
await createUser();

// Override variables
await createUser({
  variables: {
    input: {
      name: "Custom User",
      email: "custom@example.com",
    },
  },
});

TypeScript Types

Use TypedDocumentNode instead of generic type parameters:

import { gql, TypedDocumentNode } from "@apollo/client";

interface CreateUserData {
  createUser: {
    id: string;
    name: string;
    email: string;
  };
}

interface CreateUserVariables {
  input: {
    name: string;
    email: string;
  };
}

const CREATE_USER: TypedDocumentNode<CreateUserData, CreateUserVariables> = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

const [createUser, { data, loading }] = useMutation(CREATE_USER);

const { data } = await createUser({
  variables: {
    input: { name: "John", email: "john@example.com" },
  },
});

// data.createUser is fully typed

Loading and Error States

Handling in UI

function CreatePost() {
  const [createPost, { loading, error, data, reset }] =
    useMutation(CREATE_POST);

  if (data) {
    return (
      <div>
        <p>Post created: {data.createPost.title}</p>
        <button onClick={reset}>Create another</button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" disabled={loading} />
      <textarea name="content" disabled={loading} />
      <button type="submit" disabled={loading}>
        {loading ? "Creating..." : "Create Post"}
      </button>
      {error && (
        <div className="error">
          <p>Failed to create post: {error.message}</p>
          <button onClick={reset}>Try again</button>
        </div>
      )}
    </form>
  );
}

Async/Await Pattern

If you only need the promise without using the hook's loading/data state, use client.mutate instead:

import { useApolloClient } from "@apollo/client/react";

function CreatePost() {
  const client = useApolloClient();

  async function handleSubmit(formData: FormData) {
    try {
      const { data } = await client.mutate({
        mutation: CREATE_POST,
        variables: {
          input: {
            title: formData.get("title"),
            content: formData.get("content"),
          },
        },
      });
      console.log("Created:", data.createPost);
      router.push(`/posts/${data.createPost.id}`);
    } catch (error) {
      console.error("Failed to create post:", error);
    }
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        handleSubmit(new FormData(e.currentTarget));
      }}
    >
      ...
    </form>
  );
}

If you do use the hook's state, e.g. because you want to render the loading state, errors or returned data, you can also use the useMutation hook with async..await in your handler:

function CreatePost() {
  const [createPost, { loading }] = useMutation(CREATE_POST);

  async function handleSubmit(formData: FormData) {
    try {
      const { data } = await createPost({
        variables: {
          input: {
            title: formData.get("title"),
            content: formData.get("content"),
          },
        },
      });
      console.log("Created:", data.createPost);
      router.push(`/posts/${data.createPost.id}`);
    } catch (error) {
      console.error("Failed to create post:", error);
    }
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        handleSubmit(new FormData(e.currentTarget));
      }}
    >
      <button type="submit" disabled={loading}>
        {loading ? "Creating..." : "Create Post"}
      </button>
    </form>
  );
}

Optimistic UI

Optimistic UI immediately reflects the expected result of a mutation before the server responds.

Basic Optimistic Response

Important: optimisticResponse needs to be a full valid response for the mutation. A partial result might result in subtle errors.

const [addTodo] = useMutation(ADD_TODO, {
  optimisticResponse: {
    addTodo: {
      __typename: "Todo",
      id: "temp-id",
      text: "New todo",
      completed: false,
    },
  },
});

Dynamic Optimistic Response

function TodoList() {
  const [addTodo] = useMutation(ADD_TODO);

  const handleAdd = (text: string) => {
    addTodo({
      variables: { text },
      optimisticResponse: {
        addTodo: {
          __typename: "Todo",
          id: `temp-${Date.now()}`,
          text,
          completed: false,
        },
      },
    });
  };

  return <AddTodoForm onAdd={handleAdd} />;
}

Optimistic Response with Cache Update

const [toggleTodo] = useMutation(TOGGLE_TODO, {
  optimisticResponse: ({ id }) => ({
    toggleTodo: {
      __typename: "Todo",
      id,
      completed: true, // Assume success
    },
  }),
  update: (cache, { data }) => {
    // This runs twice: once with optimistic data, once with server data
    cache.modify({
      id: cache.identify(data.toggleTodo),
      fields: {
        completed: () => data.toggleTodo.completed,
      },
    });
  },
});

Cache Updates

Using update Function

const [addTodo] = useMutation(ADD_TODO, {
  update: (cache, { data }) => {
    // Read existing todos from cache
    const existingTodos = cache.readQuery<{ todos: Todo[] }>({
      query: GET_TODOS,
    });

    // Write updated list back to cache
    cache.writeQuery({
      query: GET_TODOS,
      data: {
        todos: [...(existingTodos?.todos ?? []), data.addTodo],
      },
    });
  },
});

cache.modify

const [deleteTodo] = useMutation(DELETE_TODO, {
  update: (cache, { data }) => {
    cache.modify({
      fields: {
        todos: (existingTodos: Reference[], { readField }) => {
          return existingTodos.filter(
            (todoRef) => readField("id", todoRef) !== data.deleteTodo.id
          );
        },
      },
    });
  },
});

cache.evict

const [deleteUser] = useMutation(DELETE_USER, {
  update: (cache, { data }) => {
    // Remove the user object from cache entirely
    cache.evict({ id: cache.identify(data.deleteUser) });
    // Clean up dangling references
    cache.gc();
  },
});

Updating Related Queries

const [createPost] = useMutation(CREATE_POST, {
  update: (cache, { data }) => {
    // Update author's post count
    cache.modify({
      id: cache.identify({ __typename: "User", id: data.createPost.authorId }),
      fields: {
        postCount: (existing) => existing + 1,
        posts: (existing, { toReference }) => [
          ...existing,
          toReference(data.createPost),
        ],
      },
    });

    // Add to feed
    cache.modify({
      fields: {
        feed: (existing, { toReference }) => [
          toReference(data.createPost),
          ...existing,
        ],
      },
    });
  },
});

Refetch Queries

Basic Refetch

There are three refetch notations:

  • String: refetchQueries: ['getTodos'] - refetches all active getTodos queries
  • Query document: refetchQueries: [GET_TODOS] - refetches all active queries using this document
  • Object: refetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }] - fetches the query, regardless if it's actively used in the UI
const [addTodo] = useMutation(ADD_TODO, {
  // Refetch all active GET_TODOS queries
  refetchQueries: ["getTodos"],
  // Or: refetchQueries: [GET_TODOS],
});

// Fetch specific query with variables (even if not active)
const [addTodo] = useMutation(ADD_TODO, {
  refetchQueries: [{ query: GET_TODOS }, { query: GET_TODO_COUNT }],
});

Conditional Refetch

const [addTodo] = useMutation(ADD_TODO, {
  refetchQueries: (result) => {
    if (result.data?.addTodo.priority === "HIGH") {
      return [{ query: GET_HIGH_PRIORITY_TODOS }];
    }
    return [{ query: GET_TODOS }];
  },
});

Refetch Active Queries

const [addTodo] = useMutation(ADD_TODO, {
  refetchQueries: "active", // Refetch all active queries
  // Or: 'all' to refetch all queries (including inactive)
});

awaitRefetchQueries

const [addTodo] = useMutation(ADD_TODO, {
  refetchQueries: [{ query: GET_TODOS }],
  awaitRefetchQueries: true, // Wait for refetch before resolving mutation
});

onQueryUpdated

Returning true from onQueryUpdated causes a refetch. Don't call refetch() manually inside onQueryUpdated, as it won't retain the query and might cancel it early.

const [addTodo] = useMutation(ADD_TODO, {
  update: (cache, { data }) => {
    // Update cache...
  },
  onQueryUpdated: (observableQuery) => {
    // Called for each query affected by cache update
    console.log(`Query ${observableQuery.queryName} was updated`);
    // Return true to refetch
    return true;
  },
});

Error Handling

Error Policy

const [createUser, { loading }] = useMutation(CREATE_USER, {
  errorPolicy: "all", // Return both data and errors
});

const { data, errors } = await createUser({
  variables: { input },
});

// Handle partial success
if (data?.createUser) {
  console.log("User created:", data.createUser);
}
if (errors) {
  console.warn("Some errors occurred:", errors);
}

onError Callback

const [createUser] = useMutation(CREATE_USER, {
  onError: (error) => {
    // Handle error globally
    toast.error(`Failed to create user: ${error.message}`);

    // Log to error tracking service
    Sentry.captureException(error);
  },
  onCompleted: (data) => {
    toast.success(`User ${data.createUser.name} created!`);
  },
});

Field-Level Errors

const [createUser] = useMutation(CREATE_USER, {
  errorPolicy: "all",
});

const handleSubmit = async (input: CreateUserInput) => {
  const { data, errors } = await createUser({
    variables: { input },
  });

  // Handle GraphQL validation errors
  const fieldErrors = errors?.reduce(
    (acc, error) => {
      const field = error.extensions?.field as string;
      if (field) {
        acc[field] = error.message;
      }
      return acc;
    },
    {} as Record<string, string>
  );

  if (fieldErrors?.email) {
    setEmailError(fieldErrors.email);
  }
};

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill is a comprehensive reference and integration guide for Apollo Client 4.x in React applications. No security vulnerabilities, malicious code, or adversarial patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    4/14 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at cb48e40. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 5 months ago
What it can do
Runs commands
metadata
{
  "author": "apollographql",
  "version": "1.0.0"
}
All 1 allowed tools
Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
Other metadata
compatibility
React 18+, React 19 (Suspense/RSC). Works with Next.js, Vite, CRA, and other React frameworks.
  • React
  • TypeScript
  • apollo-client
  • graphql
  • state-management
  • caching
  • queries
  • mutations
  • suspense

README badge

README badge for apollographql/skills/apollo-client

Guides for building React applications with Apollo Client 4.x, covering queries, mutations, caching, local state, and Suspense integration. Targets React 18+, React 19, Next.js, Vite, and other React frameworks with patterns for client-side apps, server-side rendering, and modern data fetching.

Generated from the current SKILL.md.

Does this skill work with React Server Components and Next.js App Router?
Yes. The skill includes integration guides for Next.js App Router with React Server Components, and supports React 19 with Suspense.
What Apollo Client version does this skill target?
Apollo Client 4.x specifically. The skill does not cover v3 or earlier patterns.
Does this skill cover TypeScript code generation for GraphQL operations?
Yes. The skill includes a reference section on TypeScript Code Generator setup for type-safe operations.
Can I use this skill to configure caching and optimize performance?
Yes. The skill covers InMemoryCache configuration, typePolicies, fetchPolicy strategies, and performance patterns including @defer and @stream.
What frameworks does this skill support beyond client-side React?
It includes integration guides for Next.js App Router, React Router 7 with streaming SSR, and TanStack Start, in addition to client-side apps built with Vite or Create React App.

Generated from the current SKILL.md. These answers refresh after source changes.