Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Here’s how to effectively integrate Apollo Client in your React applications.
1. Setup and Configuration
Basic Apollo Client Setup
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<YourAppComponent />
</ApolloProvider>
);
}
Advanced Configuration
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: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
2. Querying Data
Basic Query
import { gql, useQuery } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
author {
name
}
}
}
`;
function PostList() {
const { loading, error, data } = useQuery(GET_POSTS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By {post.author.name}</p>
</li>
))}
</ul>
);
}
Paginated Query
const GET_PAGINATED_POSTS = gql`
query GetPaginatedPosts($offset: Int!, $limit: Int!) {
posts(offset: $offset, limit: $limit) {
id
title
}
}
`;
function PaginatedPostList() {
const { data, loading, fetchMore } = useQuery(GET_PAGINATED_POSTS, {
variables: { offset: 0, limit: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
offset: data.posts.length,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: [...prev.posts, ...fetchMoreResult.posts],
};
},
});
};
return (
<div>
{/* Render posts */}
<button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</button>
</div>
);
}
3. Mutations
Basic Mutation
import { gql, useMutation } from '@apollo/client';
const ADD_POST = gql`
mutation AddPost($title: String!, $content: String!) {
addPost(title: $title, content: $content) {
id
title
content
}
}
`;
function AddPostForm() {
const [addPost, { loading, error }] = useMutation(ADD_POST, {
refetchQueries: ['GetPosts'], // Refetch after mutation
});
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
addPost({ variables: { title, content } });
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
/>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
Optimistic UI Update
const ADD_COMMENT = gql`
mutation AddComment($postId: ID!, $content: String!) {
addComment(postId: $postId, content: $content) {
id
content
createdAt
}
}
`;
function CommentForm({ postId }) {
const [addComment] = useMutation(ADD_COMMENT, {
optimisticResponse: {
addComment: {
id: 'temp-id',
content: 'Optimistic comment',
createdAt: new Date().toISOString(),
__typename: 'Comment',
},
},
update(cache, { data: { addComment } }) {
cache.modify({
fields: {
comments(existingComments = []) {
const newCommentRef = cache.writeFragment({
data: addComment,
fragment: gql`
fragment NewComment on Comment {
id
content
createdAt
}
`,
});
return [...existingComments, newCommentRef];
},
},
});
},
});
// Form implementation...
}
4. Local State Management
Managing Local State
const GET_VISIBILITY = gql`
query GetVisibility {
visibilityFilter @client
}
`;
const SET_VISIBILITY = gql`
mutation SetVisibility($filter: String!) {
setVisibilityFilter(filter: $filter) @client
}
`;
function VisibilityFilter() {
const { data } = useQuery(GET_VISIBILITY);
const [setVisibility] = useMutation(SET_VISIBILITY);
return (
<div>
<button onClick={() => setVisibility({ variables: { filter: 'SHOW_ALL' } })}>
All
</button>
<button onClick={() => setVisibility({ variables: { filter: 'SHOW_ACTIVE' } })}>
Active
</button>
</div>
);
}
// In your Apollo Client setup
const client = new ApolloClient({
// ...other config
resolvers: {
Mutation: {
setVisibilityFilter: (_, { filter }, { cache }) => {
cache.writeQuery({
query: GET_VISIBILITY,
data: { visibilityFilter: filter },
});
return null;
},
},
},
});
5. Advanced Features
Error Handling
import { ApolloError } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
// Add to your Apollo Client configuration
const client = new ApolloClient({
link: from([errorLink, authLink.concat(httpLink)]),
// ...other config
});
Real-time Updates with Subscriptions
import { split, HttpLink, ApolloClient } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
const wsLink = new WebSocketLink({
uri: 'wss://your-graphql-endpoint.com/subscriptions',
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem('authToken'),
},
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// Subscription component
const NEW_COMMENTS = gql`
subscription OnNewComment($postId: ID!) {
newComment(postId: $postId) {
id
content
author {
name
}
}
}
`;
function CommentSubscription({ postId }) {
const { data, loading } = useSubscription(NEW_COMMENTS, {
variables: { postId },
onSubscriptionData: ({ client, subscriptionData }) => {
// Handle new comment
},
});
// Render comments...
}
6. Performance Optimization
Query Batching
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const batchLink = new BatchHttpLink({
uri: 'https://api.example.com/graphql',
batchMax: 5, // Max operations per batch
batchInterval: 20, // Wait time in ms
});
const client = new ApolloClient({
link: batchLink,
cache: new InMemoryCache(),
});
Persisted Queries
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueriesLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true,
});
const client = new ApolloClient({
link: persistedQueriesLink.concat(httpLink),
cache: new InMemoryCache(),
});
Cache Normalization
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Product: {
keyFields: ["id", "sku"], // Multiple key fields
},
User: {
fields: {
posts: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
});
7. Testing Components
Mocking Apollo Client in Tests
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
const mocks = [
{
request: {
query: GET_POSTS,
},
result: {
data: {
posts: [
{ id: '1', title: 'Test Post', author: { name: 'Test User' } },
],
},
},
},
];
test('renders posts', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<PostList />
</MockedProvider>
);
expect(await screen.findByText('Test Post')).toBeInTheDocument();
});
Testing Mutations
const mutationMock = [
{
request: {
query: ADD_POST,
variables: { title: 'Test', content: 'Content' },
},
result: {
data: {
addPost: { id: '1', title: 'Test', content: 'Content' },
},
},
},
];
test('submits mutation', async () => {
render(
<MockedProvider mocks={mutationMock}>
<AddPostForm />
</MockedProvider>
);
fireEvent.change(screen.getByPlaceholderText('Title'), {
target: { value: 'Test' },
});
fireEvent.click(screen.getByText('Add Post'));
await waitFor(() => {
expect(mutationMock[0].result).toHaveBeenCalled();
});
});
Best Practices
- Query Colocation: Keep queries close to components that use them
- Fragment Usage: Reuse common field selections with fragments
- Error Boundaries: Wrap components with error boundaries
- Type Generation: Use GraphQL Code Generator for TypeScript types
- Pagination: Implement cursor-based pagination for large datasets
- Cache Strategy: Choose appropriate fetch policies per use case
- Bundle Optimization: Load only necessary Apollo Client features
By following these patterns and leveraging Apollo Client’s powerful features, you can build robust, performant React applications with GraphQL that scale effectively.