Skip to main content
Technology & EngineeringGraphql355 lines

Apollo Client

Apollo Client with React for querying, mutating, and caching GraphQL data

Quick Summary17 lines
You are an expert in Apollo Client v3 with React, helping developers build efficient, type-safe frontends that consume GraphQL APIs with optimistic UI, caching, and state management.

## Key Points

1. **Colocate fragments with components** — each component defines a fragment describing exactly the data it needs; parent queries compose these fragments.
2. **Use `cache-and-network` fetch policy** — show stale data immediately while fetching fresh data in the background for the best user experience.
3. **Prefer cache updates over refetching** — after mutations, update the cache directly instead of calling `refetchQueries` to avoid extra network roundtrips.
4. **Type your operations** — use GraphQL Code Generator to produce TypeScript types for every query, mutation, and fragment.
5. **Normalize IDs consistently** — ensure every type with an `id` field is properly identified by the cache. Configure `typePolicies` for types with non-standard IDs.
6. **Use `errorPolicy: "all"`** — this returns both data and errors, enabling partial rendering when only some fields fail.
- **Forgetting `__typename` in optimistic responses** — Apollo uses `__typename` to normalize cache entries. Omitting it in optimistic responses causes cache misses.
- **Stale closures in update functions** — the `update` function captures variables from render scope. Use refs or pass variables through mutation options to avoid stale state.
- **Over-fetching with fetch policies** — `network-only` on every query negates the benefit of caching. Use it sparingly for data that must always be fresh.
- **Not handling loading AND data states** — with `cache-and-network`, `loading` can be true while `data` is available from cache. Always check both.
- **Directly reading/writing cache without `cache.modify` or `cache.updateQuery`** — manual `readQuery`/`writeQuery` is error-prone and may not trigger re-renders correctly.
skilldb get graphql-skills/Apollo ClientFull skill: 355 lines
Paste into your CLAUDE.md or agent config

Apollo Client — GraphQL

You are an expert in Apollo Client v3 with React, helping developers build efficient, type-safe frontends that consume GraphQL APIs with optimistic UI, caching, and state management.

Overview

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Its normalized cache, declarative data fetching via React hooks, and built-in support for optimistic UI make it the most popular GraphQL client for React applications.

Core Concepts

Client Setup

import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

const httpLink = createHttpLink({
  uri: "https://api.example.com/graphql",
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("authToken");
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: ["filter"],
            merge(existing, incoming, { args }) {
              if (!args?.after) return incoming;
              return {
                ...incoming,
                edges: [...(existing?.edges ?? []), ...incoming.edges],
              };
            },
          },
        },
      },
    },
  }),
});

// Wrap your app
function App() {
  return (
    <ApolloProvider client={client}>
      <Router />
    </ApolloProvider>
  );
}

useQuery Hook

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

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      displayName
      email
      avatar
      posts(first: 10) {
        edges {
          node {
            id
            title
            createdAt
          }
        }
      }
    }
  }
`;

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error, refetch } = useQuery(GET_USER, {
    variables: { id: userId },
    fetchPolicy: "cache-and-network", // Show cached data while refetching
    errorPolicy: "all", // Return partial data alongside errors
  });

  if (loading && !data) return <Skeleton />;
  if (error && !data) return <ErrorDisplay error={error} retry={refetch} />;

  const user = data!.user;
  return (
    <div>
      <h1>{user.displayName}</h1>
      <p>{user.email}</p>
      <PostList posts={user.posts.edges.map((e) => e.node)} />
    </div>
  );
}

useMutation Hook

const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      post {
        id
        title
        body
        status
        createdAt
        author {
          id
          displayName
        }
      }
      errors {
        field
        message
        code
      }
    }
  }
`;

function CreatePostForm() {
  const [createPost, { loading }] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      if (!createPost.post) return; // Validation errors

      cache.modify({
        fields: {
          posts(existingPosts = { edges: [] }) {
            const newPostRef = cache.writeFragment({
              data: createPost.post,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                  body
                  status
                  createdAt
                }
              `,
            });
            return {
              ...existingPosts,
              edges: [{ __typename: "PostEdge", node: newPostRef }, ...existingPosts.edges],
            };
          },
        },
      });
    },
  });

  const handleSubmit = async (values: FormValues) => {
    const { data } = await createPost({
      variables: { input: values },
    });

    if (data?.createPost.errors.length) {
      // Handle validation errors
      return data.createPost.errors;
    }
    // Success — navigate to the new post
  };

  return <PostForm onSubmit={handleSubmit} submitting={loading} />;
}

Implementation Patterns

Optimistic UI Updates

Show immediate feedback before the server responds:

const [toggleLike] = useMutation(TOGGLE_LIKE, {
  optimisticResponse: ({ postId }) => ({
    toggleLike: {
      __typename: "ToggleLikePayload",
      post: {
        __typename: "Post",
        id: postId,
        isLikedByViewer: true,
        likeCount: currentLikeCount + 1,
      },
    },
  }),
});

Fragments for Component Data Requirements

Colocate data requirements with the component that uses them:

// UserAvatar.tsx
export const USER_AVATAR_FRAGMENT = gql`
  fragment UserAvatar on User {
    id
    displayName
    avatar
  }
`;

function UserAvatar({ user }: { user: UserAvatarFragment }) {
  return <img src={user.avatar} alt={user.displayName} />;
}

// PostCard.tsx — compose fragments
const POST_CARD_FRAGMENT = gql`
  ${USER_AVATAR_FRAGMENT}
  fragment PostCard on Post {
    id
    title
    excerpt
    createdAt
    author {
      ...UserAvatar
    }
  }
`;

// Query composes all fragments
const GET_FEED = gql`
  ${POST_CARD_FRAGMENT}
  query GetFeed($first: Int!, $after: String) {
    feed(first: $first, after: $after) {
      edges {
        node {
          ...PostCard
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

Pagination with fetchMore

function Feed() {
  const { data, loading, fetchMore } = useQuery(GET_FEED, {
    variables: { first: 20 },
  });

  const loadMore = () => {
    if (!data?.feed.pageInfo.hasNextPage) return;

    fetchMore({
      variables: {
        after: data.feed.pageInfo.endCursor,
      },
      // merge function defined in typePolicies handles combining results
    });
  };

  return (
    <div>
      {data?.feed.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}
      {data?.feed.pageInfo.hasNextPage && (
        <button onClick={loadMore} disabled={loading}>
          Load more
        </button>
      )}
    </div>
  );
}

Error Link for Global Error Handling

import { onError } from "@apollo/client/link/error";

const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    for (const err of graphQLErrors) {
      if (err.extensions?.code === "UNAUTHENTICATED") {
        // Redirect to login or refresh token
        refreshToken();
      }
    }
  }

  if (networkError) {
    console.error(`[Network error on ${operation.operationName}]:`, networkError);
  }
});

const client = new ApolloClient({
  link: ApolloLink.from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache(),
});

Core Philosophy

Apollo Client succeeds when it becomes the single source of truth for both remote and local data. Rather than treating the cache as a passive layer that mirrors server responses, think of it as a normalized, client-side database that your components subscribe to. Every query result is decomposed into individually cached entities keyed by __typename and id, so updating one entity automatically re-renders every component that references it. This mental model — cache as database, components as subscribers — is the key to avoiding redundant fetches and stale UI.

The strongest Apollo Client codebases colocate data requirements with the components that consume them using fragments. When each component declares exactly which fields it needs, the composition happens naturally: parent queries assemble child fragments, and refactoring a component's data needs is a local change rather than a system-wide audit. This pattern also unlocks fragment masking, where TypeScript enforces that a component can only access the fields it explicitly requested.

Performance-aware teams lean into the cache rather than fighting it. Instead of calling refetchQueries after every mutation, they write cache updates that apply the mutation result directly, keeping the UI responsive and the network quiet. The cache-and-network fetch policy lets users see instant results from cache while a background refresh ensures freshness — the best of both worlds for most CRUD interfaces.

Anti-Patterns

  • Cache-busting with network-only everywhere — defaulting every query to network-only because "the cache is confusing" throws away Apollo's primary advantage. It turns your GraphQL client into a glorified fetch wrapper, adding latency to every navigation and wasting bandwidth on data that has not changed.

  • God queries at the page level — fetching all data for an entire page in a single monolithic query creates tight coupling between unrelated components. When one field changes, the entire query must be re-evaluated. Break queries into composable fragments owned by individual components.

  • Manual readQuery/writeQuery gymnastics — complex manual cache reads and writes are fragile, hard to test, and break silently when the schema evolves. Prefer cache.modify for targeted field updates and cache.updateQuery for structural changes.

  • Ignoring partial error states — treating error as a binary "everything failed" signal discards usable data. With errorPolicy: "all", you can render the fields that succeeded and show localized error messages for the fields that did not.

  • Prop-drilling query results instead of leveraging the cache — passing fetched data through multiple component layers defeats the purpose of a normalized cache. Child components should run their own useQuery calls or read fragments directly from the cache, trusting Apollo to deduplicate and batch.

Best Practices

  1. Colocate fragments with components — each component defines a fragment describing exactly the data it needs; parent queries compose these fragments.
  2. Use cache-and-network fetch policy — show stale data immediately while fetching fresh data in the background for the best user experience.
  3. Prefer cache updates over refetching — after mutations, update the cache directly instead of calling refetchQueries to avoid extra network roundtrips.
  4. Type your operations — use GraphQL Code Generator to produce TypeScript types for every query, mutation, and fragment.
  5. Normalize IDs consistently — ensure every type with an id field is properly identified by the cache. Configure typePolicies for types with non-standard IDs.
  6. Use errorPolicy: "all" — this returns both data and errors, enabling partial rendering when only some fields fail.

Common Pitfalls

  • Forgetting __typename in optimistic responses — Apollo uses __typename to normalize cache entries. Omitting it in optimistic responses causes cache misses.
  • Stale closures in update functions — the update function captures variables from render scope. Use refs or pass variables through mutation options to avoid stale state.
  • Over-fetching with fetch policiesnetwork-only on every query negates the benefit of caching. Use it sparingly for data that must always be fresh.
  • Not handling loading AND data states — with cache-and-network, loading can be true while data is available from cache. Always check both.
  • Directly reading/writing cache without cache.modify or cache.updateQuery — manual readQuery/writeQuery is error-prone and may not trigger re-renders correctly.

Install this skill directly: skilldb add graphql-skills

Get CLI access →