GraphQL with React and Apollo Client
Consume GraphQL APIs from React using Apollo Client, understand queries, mutations, loading and error states, normalized caching, variables, refetching, and client-side data management.
Understanding Apollo Client
Apollo Client is a client-side GraphQL library that helps React applications execute operations and manage GraphQL data.
A GraphQL client is responsible for more than sending HTTP requests. Apollo Client can provide:
- Query execution
- Mutation execution
- Request state management
- Normalized caching
- Cache updates
- Optimistic UI
- Polling
- Refetching
- Pagination helpers
- Error handling
A useful mental model is:
React Component
↓
Apollo Client
↓
Cache ────────┐
↓ │
GraphQL Link │
↓ │
GraphQL API │
↓ │
Response ────┘
Apollo Client can first satisfy a query from its cache depending on the configured fetch policy and then communicate with the server when required.
This is one of the major differences between using a raw fetch() call and using a full GraphQL client library.
- Apollo Client is more than a GraphQL HTTP wrapper.
- It can manage server data and a normalized client cache.
- Fetch policies determine how cache and network are combined.
- React components can subscribe to GraphQL query results.
Which capability distinguishes Apollo Client from simply using `fetch()` for GraphQL requests?
Setting Up Apollo Client in React
A React application typically creates an Apollo Client instance and provides it to the component tree.
Conceptually:
const client = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache()
});
The application then provides the client through Apollo's React integration.
<ApolloProvider client={client}>
<App />
</ApolloProvider>
Components below the provider can use GraphQL hooks such as useQuery and useMutation.
The InMemoryCache is a normalized cache. It can store entities and references rather than simply caching entire query responses as opaque blobs.
In production applications, the Apollo Client setup often also includes an authentication link, error handling, retry behavior, upload support, tracing headers, or other transport concerns.
Keep transport configuration centralized instead of recreating clients inside individual React components.
import { ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache()
});
export default client;- Create a shared Apollo Client instance.
- Use
InMemoryCachefor Apollo's normalized cache. - Provide the client to React through the Apollo provider.
- Centralize authentication and transport configuration.
Why should an Apollo Client instance generally be shared rather than recreated for every React component?
Fetching GraphQL Data with useQuery
Apollo Client's useQuery hook connects a React component to a GraphQL query.
For example:
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
A component can use it:
const { data, loading, error } = useQuery(GET_USER, {
variables: { id }
});
The component should account for at least three broad states:
Loading
↓
Success ─────→ Render data
↓
Error ───────→ Render error state
A production component should not assume data exists immediately.
For example:
if (loading) return <Spinner />;
if (error) return <ErrorMessage />;
if (!data?.user) return <NotFound />;
return <UserProfile user={data.user} />;
The exact UI depends on the application, but the important concept is that server state is asynchronous and can change independently of the component lifecycle.
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ id }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id }
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Failed to load user.</p>;
if (!data?.user) return <p>User not found.</p>;
return <h1>{data.user.name}</h1>;
}useQueryconnects a React component to a GraphQL query.- Variables are passed separately from the GraphQL document.
- Components should handle loading and error states.
- Server data should not be assumed to exist immediately.
Which state should a React component generally consider when using `useQuery`?
Apollo Fetch Policies
Apollo Client provides fetch policies that determine how a query interacts with the cache and network.
Common policies include:
cache-first
Use cached data when available. Otherwise request from the network.
network-only
Always request fresh data from the network and update the cache with the result.
cache-only
Read from the cache without making a network request.
no-cache
Request from the network without storing the result in the cache in the normal way.
cache-and-network
Return cached data when available while also requesting fresh network data.
The correct policy depends on the data.
For relatively stable reference data, cache-first can reduce unnecessary requests.
For rapidly changing data such as a live operational dashboard, network-only or cache-and-network may be more appropriate.
Do not choose network-only everywhere simply because freshness sounds safer. Doing so can throw away many of the benefits of client-side caching.
Conversely, do not use cache-first for data where stale information can create serious business problems without considering how and when the cache is invalidated.
Cache first, network if missingAlways fetch from networkCache immediately and refresh from networkCache onlyNetwork without normal cache storage- Fetch policies control cache and network behavior.
- No single fetch policy is correct for every query.
- Freshness requirements should drive policy decisions.
- Cache behavior and invalidation are part of application design.
Which fetch policy requests network data while also allowing cached data to be displayed first when available?
Executing Mutations from React
Apollo Client's mutation APIs allow React components to trigger GraphQL mutations.
For example:
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
`;
The component can call:
const [createUser, { loading, error }] = useMutation(CREATE_USER);
Then:
await createUser({
variables: {
input: {
name,
email
}
}
});
After a mutation, the client cache may need to be updated.
Suppose the application has:
query Users {
users {
id
name
}
}
Creating a user does not necessarily mean Apollo can infer how your specific UI should update every query. Depending on the schema and cache configuration, you may need to use cache updates, refetching, or mutation result normalization.
A good client architecture treats mutation completion and cache consistency as explicit concerns.
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading }] = useMutation(CREATE_USER);
const submit = async () => {
await createUser({
variables: {
input: {
name: "Alice",
email: "alice@example.com"
}
}
});
};
return <button disabled={loading} onClick={submit}>Create</button>;
}- Mutations are triggered explicitly by application actions.
- Mutation variables are supplied separately.
- Mutation completion may require cache updates.
- Loading and error states should be represented in the UI.
Why might a React application need to update the Apollo cache after a successful mutation?