Apollo Client
Apollo Client with React for querying, mutating, and caching GraphQL data
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 linesApollo 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-onlyeverywhere — defaulting every query tonetwork-onlybecause "the cache is confusing" throws away Apollo's primary advantage. It turns your GraphQL client into a glorifiedfetchwrapper, 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/writeQuerygymnastics — complex manual cache reads and writes are fragile, hard to test, and break silently when the schema evolves. Prefercache.modifyfor targeted field updates andcache.updateQueryfor structural changes. -
Ignoring partial error states — treating
erroras a binary "everything failed" signal discards usable data. WitherrorPolicy: "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
useQuerycalls or read fragments directly from the cache, trusting Apollo to deduplicate and batch.
Best Practices
- Colocate fragments with components — each component defines a fragment describing exactly the data it needs; parent queries compose these fragments.
- Use
cache-and-networkfetch policy — show stale data immediately while fetching fresh data in the background for the best user experience. - Prefer cache updates over refetching — after mutations, update the cache directly instead of calling
refetchQueriesto avoid extra network roundtrips. - Type your operations — use GraphQL Code Generator to produce TypeScript types for every query, mutation, and fragment.
- Normalize IDs consistently — ensure every type with an
idfield is properly identified by the cache. ConfiguretypePoliciesfor types with non-standard IDs. - Use
errorPolicy: "all"— this returns both data and errors, enabling partial rendering when only some fields fail.
Common Pitfalls
- Forgetting
__typenamein optimistic responses — Apollo uses__typenameto normalize cache entries. Omitting it in optimistic responses causes cache misses. - Stale closures in update functions — the
updatefunction captures variables from render scope. Use refs or pass variables through mutation options to avoid stale state. - Over-fetching with fetch policies —
network-onlyon 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,loadingcan be true whiledatais available from cache. Always check both. - Directly reading/writing cache without
cache.modifyorcache.updateQuery— manualreadQuery/writeQueryis error-prone and may not trigger re-renders correctly.
Install this skill directly: skilldb add graphql-skills
Related Skills
Apollo Server
Apollo Server setup, configuration, plugins, and production deployment patterns
Authentication
Authentication and authorization patterns for securing GraphQL APIs
Code Generation
Type-safe GraphQL development with graphql-codegen for TypeScript
Pagination
Cursor-based pagination following the Relay connection specification
Resolvers
Resolver patterns, data loading strategies, and the N+1 problem in GraphQL
Schema Design
Schema design principles for building maintainable, intuitive GraphQL APIs