@apollo/client
Apollo Client is a GraphQL client and normalized in-memory cache for JavaScript applications, with official React bindings and framework-neutral core APIs. It executes queries, mutations, subscriptions, and incremental responses, then normalizes objects so later operations and local cache reads can share data. Its main value is coordinated GraphQL state rather than HTTP transport: fetch policies, field policies, optimistic writes, pagination merges, reactive updates, and developer tools all sit on top of the operation layer.
Apollo Client is worth its weight when a real normalized GraphQL cache coordinates a large UI. For a handful of requests or mixed REST applications, use a smaller client and avoid making cache policy a second data-modeling job.
Use it if
- Your React application has many GraphQL screens that should share a normalized entity cache
- You need optimistic mutations, pagination field policies, subscriptions, or fine-grained cache updates
- Your team wants mature React hooks, Suspense support, developer tools, and extensive GraphQL documentation
- You consume a schema with stable object identifiers and can adopt generated typed documents
- You make a few independent GraphQL requests and do not need normalized caching; `graphql-request` or fetch is much smaller and easier to reason about
- Bundle weight is tight: the current package measures about 35.5 KB gzipped before your GraphQL documents and optional subscription packages
- Your API is mainly REST or server actions; Apollo's operation documents, normalized cache, links, and provider add machinery without matching the data model
- You cannot upgrade to GraphQL 16 or 17 or RxJS 7; version 4.2.10 declares both as required peer dependencies
- Your release policy assumes minor versions only add features: Apollo's README says minors may change transpilation targets, update dependencies, or drop support for older dependency versions
Setup reality
For Apollo Client 4, install `@apollo/client`, `graphql`, and a compatible RxJS 7 peer. React, React DOM, and websocket transports are optional peers, but a React app supplies its own compatible React 17, 18, or 19 packages. Version 4 moved React exports to `@apollo/client/react`; copying v3 examples that import `useQuery` from the package root is a common first failure. Create one `ApolloClient` with an endpoint or link plus `InMemoryCache`, then put `ApolloProvider` above every hook consumer. The hard part is cache identity. Apollo normally combines `__typename` with `id` or `_id`; objects without stable keys remain embedded, while custom identifiers need `typePolicies.keyFields`. List fields need pagination merge and `keyArgs` policies or pages overwrite each other, duplicate, or fragment into separate cache entries. Default `cache-first` behavior can look stale until fetch policies are chosen intentionally. Mutations do not magically place new list members in every cached query; refetch or update the cache and include identifiers plus `__typename` in optimistic results. Authentication, retries, uploads, persisted queries, and subscriptions require link configuration and sometimes optional packages such as `graphql-ws`. SSR and React Server Components have framework-specific integration packages and must avoid sharing one user cache across requests. The client works with any GraphQL endpoint and does not require GraphOS, but schema typing is far better with external GraphQL code generation than with hand-written result interfaces.
Patterns
Create an Apollo Client 4 instancecreate-client
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
export const client = new ApolloClient({
link: new HttpLink({ uri: 'https://api.example.com/graphql' }),
cache: new InMemoryCache(),
});Install compatible `graphql` and `rxjs` peers; GraphOS is not required for a normal GraphQL endpoint.
Provide the client to React hooksprovide-react-client
import { ApolloProvider } from '@apollo/client/react';
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
);Apollo Client 4 React exports come from `@apollo/client/react`, not the package root used by many v3 examples.
Run a GraphQL query from Reactquery-in-component
import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`;
function User({ id }) {
const { data, loading, error } = useQuery(GET_USER, { variables: { id } });
if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <h1>{data.user.name}</h1>;
}Include stable IDs in selections so `InMemoryCache` can normalize and share entity data across operations.
Skip a query until required input existsskip-query-safely
import { skipToken, useQuery } from '@apollo/client/react';
const result = useQuery(
GET_USER,
userId ? { variables: { id: userId } } : skipToken,
);`skipToken` keeps variables type-safe and avoids inventing a placeholder ID just to satisfy TypeScript.
Run a query in response to an eventrun-lazy-query
import { useLazyQuery } from '@apollo/client/react';
function SearchButton({ term }) {
const [search, { data, loading }] = useLazyQuery(SEARCH);
return <button disabled={loading} onClick={() => search({ variables: { term } })}>
{data ? `${data.search.length} results` : 'Search'}
</button>;
}Use lazy queries for imperative user events; ordinary render-driven data should stay declarative with `useQuery`.
Execute a mutation and refetch an active queryrun-mutation
import { gql } from '@apollo/client';
import { useMutation } from '@apollo/client/react';
const ADD_TODO = gql`
mutation AddTodo($text: String!) { addTodo(text: $text) { id text done } }
`;
const [addTodo, state] = useMutation(ADD_TODO, {
refetchQueries: [GET_TODOS],
});
await addTodo({ variables: { text: 'Ship it' } });Refetching is simple but costs a network round trip, and named refetch targets only affect active queries.
Show an optimistic mutation resultoptimistic-mutation
await addTodo({
variables: { text },
optimisticResponse: {
addTodo: {
__typename: 'Todo',
id: `temp:${crypto.randomUUID()}`,
text,
done: false,
},
},
});Provide `__typename` and a temporary stable ID or the optimistic object cannot normalize and reconcile cleanly.
Append a mutation result to a cached listappend-cache-item
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data }) {
if (!data?.addTodo) return;
cache.modify({
fields: {
todos(existing = [], { toReference }) {
return [...existing, toReference(data.addTodo)];
},
},
});
},
});A mutation result normalizes the entity but does not automatically add it to every cached list that should contain it.
Configure a custom entity keyconfigure-type-identity
const cache = new InMemoryCache({
typePolicies: {
Product: { keyFields: ['sku'] },
Country: { keyFields: ['code'] },
},
});Only use fields that are globally stable for that GraphQL type; a changing key creates duplicate cache entities.
Merge offset-based pagination resultsmerge-offset-pages
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
keyArgs: ['filter'],
merge(existing = [], incoming, { args }) {
const merged = existing.slice();
const offset = args?.offset ?? 0;
incoming.forEach((item, index) => { merged[offset + index] = item; });
return merged;
},
},
},
},
},
});Keep arguments that identify distinct lists in `keyArgs`; omitting a filter can merge unrelated result sets.
Fetch fresh once, then prefer cachechoose-fetch-policy
const result = useQuery(GET_DASHBOARD, {
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-first',
});The default is cache-first; choose policies deliberately or stale cache data and unnecessary refetches can both look like bugs.
Run an operation outside Reactquery-without-react
const { data } = await client.query({
query: GET_USER,
variables: { id: '42' },
fetchPolicy: 'network-only',
});Use the core client in loaders and services; calling React hooks outside a component violates React's hook rules.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| urql | npm | You want GraphQL-focused React hooks with a smaller exchange-based client |
| @tanstack/react-query | npm | You want transport-agnostic server-state caching for REST, GraphQL, or mixed APIs |
| graphql-request | npm | You only need a typed, minimal GraphQL request layer without a normalized cache |