Setting Up Apollo Client
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. It provides a powerful caching layer, declarative data fetching with React hooks, and features like optimistic UI updates and pagination.
# Install Apollo Client and GraphQL
npm install @apollo/client graphql
Client Configuration
// lib/apollo-client.ts
import {
ApolloClient,
InMemoryCache,
createHttpLink,
from,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
// HTTP connection to the GraphQL API
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:4000/graphql',
credentials: 'include',
});
// Authentication link — adds token to every request
const authLink = setContext((_, { headers }) => {
const token = typeof window !== 'undefined'
? localStorage.getItem('auth-token')
: null;
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Error handling link
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error(`[GraphQL error]: ${message}`);
if (extensions?.code === 'UNAUTHENTICATED') {
// Redirect to login or refresh token
localStorage.removeItem('auth-token');
window.location.href = '/login';
}
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
// Cache configuration
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// Merge paginated results
keyArgs: ['filter'],
merge(existing = [], incoming, { args }) {
const offset = args?.offset || 0;
const merged = existing.slice(0);
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
User: {
fields: {
posts: {
merge: false, // Always replace, don't merge
},
},
},
},
});
export const apolloClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache,
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
Provider Setup
// app/providers.tsx
'use client';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '@/lib/apollo-client';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ApolloProvider client={apolloClient}>
{children}
</ApolloProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
useQuery Hook
import { gql, useQuery } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(filter: { isPublished: true }, limit: $limit, offset: $offset) {
id
title
excerpt
author { id name avatarUrl }
publishedAt
likesCount
}
}
`;
function PostList() {
const { data, loading, error, fetchMore, refetch } = useQuery(GET_POSTS, {
variables: { limit: 10, offset: 0 },
notifyOnNetworkStatusChange: true,
});
if (loading && !data) return <PostListSkeleton />;
if (error) return <ErrorMessage error={error} retry={() => refetch()} />;
return (
<div className="space-y-4">
{data.posts.map((post: any) => (
<PostCard key={post.id} post={post} />
))}
<button
onClick={() => fetchMore({ variables: { offset: data.posts.length } })}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
{loading ? 'Loading...' : 'Load More'}
</button>
</div>
);
}
useMutation Hook
import { gql, useMutation } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
body
slug
isPublished
createdAt
}
}
`;
function CreatePostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
// Update the cache after mutation
update(cache, { data: { createPost: newPost } }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: newPost,
fragment: gql`
fragment NewPost on Post {
id
title
slug
isPublished
createdAt
}
`,
});
return [newPostRef, ...existingPosts];
},
},
});
},
onCompleted: (data) => {
router.push(`/posts/${data.createPost.slug}`);
},
onError: (error) => {
toast.error(error.message);
},
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createPost({
variables: {
input: {
title: formData.get('title'),
body: formData.get('body'),
isPublished: true,
},
},
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input name="title" placeholder="Post title" required className="w-full p-3 border rounded" />
<textarea name="body" placeholder="Write your post..." required className="w-full p-3 border rounded h-40" />
{error && <p className="text-red-500">{error.message}</p>}
<button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded">
{loading ? 'Publishing...' : 'Publish Post'}
</button>
</form>
);
}
Optimistic Updates
Optimistic updates make the UI feel instant by updating the cache before the server responds. If the mutation fails, Apollo automatically rolls back the optimistic response:
const LIKE_POST = gql`
mutation LikePost($postId: ID!) {
likePost(id: $postId) {
id
likesCount
isLikedByViewer
}
}
`;
function LikeButton({ post }: { post: { id: string; likesCount: number; isLikedByViewer: boolean } }) {
const [likePost] = useMutation(LIKE_POST, {
optimisticResponse: {
likePost: {
__typename: 'Post',
id: post.id,
likesCount: post.isLikedByViewer ? post.likesCount - 1 : post.likesCount + 1,
isLikedByViewer: !post.isLikedByViewer,
},
},
});
return (
<button
onClick={() => likePost({ variables: { postId: post.id } })}
className={`flex items-center gap-2 px-3 py-1 rounded-full ${
post.isLikedByViewer ? 'bg-red-100 text-red-600' : 'bg-gray-100 text-gray-600'
}`}
>
{post.isLikedByViewer ? '❤️' : '🤍'} {post.likesCount}
</button>
);
}
Apollo Client Best Practices
- Use cache-and-network: Show cached data immediately, then update when network responds
- Optimistic updates for mutations: Make the UI feel instant by predicting the server response
- Error boundaries: Wrap query components in React error boundaries for graceful failure
- Co-locate queries: Define queries near the components that use them, not in a centralized folder
- Use TypedDocumentNode: Enable full type safety between queries and components