Apollo Client provides powerful GraphQL capabilities for React applications. Here's how to use it effectively.
Setup and Configuration#
1// lib/apollo.ts
2import {
3 ApolloClient,
4 InMemoryCache,
5 createHttpLink,
6 from,
7} from '@apollo/client';
8import { setContext } from '@apollo/client/link/context';
9import { onError } from '@apollo/client/link/error';
10
11// HTTP link
12const httpLink = createHttpLink({
13 uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,
14});
15
16// Auth link
17const authLink = setContext((_, { headers }) => {
18 const token = localStorage.getItem('token');
19 return {
20 headers: {
21 ...headers,
22 authorization: token ? `Bearer ${token}` : '',
23 },
24 };
25});
26
27// Error link
28const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
29 if (graphQLErrors) {
30 graphQLErrors.forEach(({ message, locations, path }) => {
31 console.error(
32 `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
33 );
34 });
35 }
36
37 if (networkError) {
38 console.error(`[Network error]: ${networkError}`);
39
40 if ('statusCode' in networkError && networkError.statusCode === 401) {
41 // Handle unauthorized
42 localStorage.removeItem('token');
43 window.location.href = '/login';
44 }
45 }
46});
47
48// Cache configuration
49const cache = new InMemoryCache({
50 typePolicies: {
51 Query: {
52 fields: {
53 // Pagination
54 posts: {
55 keyArgs: ['filter'],
56 merge(existing = [], incoming, { args }) {
57 if (args?.offset === 0) {
58 return incoming;
59 }
60 return [...existing, ...incoming];
61 },
62 },
63 },
64 },
65 User: {
66 keyFields: ['id'],
67 },
68 Post: {
69 keyFields: ['id'],
70 fields: {
71 comments: {
72 merge(existing = [], incoming) {
73 return incoming;
74 },
75 },
76 },
77 },
78 },
79});
80
81export const client = new ApolloClient({
82 link: from([errorLink, authLink, httpLink]),
83 cache,
84 defaultOptions: {
85 watchQuery: {
86 fetchPolicy: 'cache-and-network',
87 },
88 },
89});Queries#
1// hooks/useUser.ts
2import { gql, useQuery } from '@apollo/client';
3
4const GET_USER = gql`
5 query GetUser($id: ID!) {
6 user(id: $id) {
7 id
8 name
9 email
10 avatar
11 posts {
12 id
13 title
14 }
15 }
16 }
17`;
18
19interface User {
20 id: string;
21 name: string;
22 email: string;
23 avatar: string;
24 posts: { id: string; title: string }[];
25}
26
27interface GetUserData {
28 user: User;
29}
30
31interface GetUserVars {
32 id: string;
33}
34
35export function useUser(id: string) {
36 const { data, loading, error, refetch } = useQuery<GetUserData, GetUserVars>(
37 GET_USER,
38 {
39 variables: { id },
40 skip: !id,
41 notifyOnNetworkStatusChange: true,
42 }
43 );
44
45 return {
46 user: data?.user,
47 loading,
48 error,
49 refetch,
50 };
51}
52
53// Component usage
54function UserProfile({ userId }: { userId: string }) {
55 const { user, loading, error } = useUser(userId);
56
57 if (loading) return <Skeleton />;
58 if (error) return <Error message={error.message} />;
59 if (!user) return <NotFound />;
60
61 return (
62 <div>
63 <h1>{user.name}</h1>
64 <p>{user.email}</p>
65 </div>
66 );
67}Mutations#
1// hooks/useCreatePost.ts
2import { gql, useMutation } from '@apollo/client';
3
4const CREATE_POST = gql`
5 mutation CreatePost($input: CreatePostInput!) {
6 createPost(input: $input) {
7 id
8 title
9 content
10 author {
11 id
12 name
13 }
14 createdAt
15 }
16 }
17`;
18
19const GET_POSTS = gql`
20 query GetPosts {
21 posts {
22 id
23 title
24 createdAt
25 }
26 }
27`;
28
29export function useCreatePost() {
30 const [createPost, { loading, error }] = useMutation(CREATE_POST, {
31 // Update cache after mutation
32 update(cache, { data: { createPost } }) {
33 const existing = cache.readQuery({ query: GET_POSTS });
34
35 if (existing) {
36 cache.writeQuery({
37 query: GET_POSTS,
38 data: {
39 posts: [createPost, ...existing.posts],
40 },
41 });
42 }
43 },
44 // Or refetch queries
45 refetchQueries: [{ query: GET_POSTS }],
46 });
47
48 const handleCreate = async (input: CreatePostInput) => {
49 try {
50 const { data } = await createPost({ variables: { input } });
51 return data.createPost;
52 } catch (err) {
53 throw err;
54 }
55 };
56
57 return { createPost: handleCreate, loading, error };
58}
59
60// Usage
61function CreatePostForm() {
62 const { createPost, loading } = useCreatePost();
63
64 const handleSubmit = async (values: FormValues) => {
65 const post = await createPost(values);
66 router.push(`/posts/${post.id}`);
67 };
68
69 return (
70 <form onSubmit={handleSubmit}>
71 {/* form fields */}
72 <button type="submit" disabled={loading}>
73 {loading ? 'Creating...' : 'Create Post'}
74 </button>
75 </form>
76 );
77}Optimistic Updates#
1// Optimistic mutation
2const TOGGLE_LIKE = gql`
3 mutation ToggleLike($postId: ID!) {
4 toggleLike(postId: $postId) {
5 id
6 isLiked
7 likeCount
8 }
9 }
10`;
11
12function useLikePost() {
13 const [toggleLike] = useMutation(TOGGLE_LIKE);
14
15 const handleLike = (post: Post) => {
16 toggleLike({
17 variables: { postId: post.id },
18 optimisticResponse: {
19 __typename: 'Mutation',
20 toggleLike: {
21 __typename: 'Post',
22 id: post.id,
23 isLiked: !post.isLiked,
24 likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1,
25 },
26 },
27 });
28 };
29
30 return { handleLike };
31}
32
33// Optimistic with cache update
34const ADD_COMMENT = gql`
35 mutation AddComment($postId: ID!, $content: String!) {
36 addComment(postId: $postId, content: $content) {
37 id
38 content
39 author {
40 id
41 name
42 }
43 createdAt
44 }
45 }
46`;
47
48function useAddComment() {
49 const [addComment] = useMutation(ADD_COMMENT, {
50 update(cache, { data: { addComment } }, { variables }) {
51 cache.modify({
52 id: cache.identify({ __typename: 'Post', id: variables.postId }),
53 fields: {
54 comments(existingComments = []) {
55 const newCommentRef = cache.writeFragment({
56 data: addComment,
57 fragment: gql`
58 fragment NewComment on Comment {
59 id
60 content
61 author {
62 id
63 name
64 }
65 createdAt
66 }
67 `,
68 });
69 return [...existingComments, newCommentRef];
70 },
71 commentCount(existing = 0) {
72 return existing + 1;
73 },
74 },
75 });
76 },
77 });
78
79 return { addComment };
80}Pagination#
1// Cursor-based pagination
2const GET_POSTS = gql`
3 query GetPosts($cursor: String, $limit: Int!) {
4 posts(cursor: $cursor, limit: $limit) {
5 edges {
6 node {
7 id
8 title
9 createdAt
10 }
11 cursor
12 }
13 pageInfo {
14 hasNextPage
15 endCursor
16 }
17 }
18 }
19`;
20
21function useInfinitePosts() {
22 const { data, loading, fetchMore } = useQuery(GET_POSTS, {
23 variables: { limit: 10 },
24 });
25
26 const loadMore = () => {
27 if (!data?.posts.pageInfo.hasNextPage) return;
28
29 fetchMore({
30 variables: {
31 cursor: data.posts.pageInfo.endCursor,
32 limit: 10,
33 },
34 });
35 };
36
37 return {
38 posts: data?.posts.edges.map((e) => e.node) ?? [],
39 hasMore: data?.posts.pageInfo.hasNextPage ?? false,
40 loading,
41 loadMore,
42 };
43}
44
45// Cache merge policy for pagination
46const cache = new InMemoryCache({
47 typePolicies: {
48 Query: {
49 fields: {
50 posts: {
51 keyArgs: false,
52 merge(existing, incoming, { args }) {
53 if (!args?.cursor) {
54 return incoming;
55 }
56 return {
57 ...incoming,
58 edges: [...(existing?.edges || []), ...incoming.edges],
59 };
60 },
61 },
62 },
63 },
64 },
65});Subscriptions#
1// Setup WebSocket link
2import { split } from '@apollo/client';
3import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
4import { getMainDefinition } from '@apollo/client/utilities';
5import { createClient } from 'graphql-ws';
6
7const wsLink = new GraphQLWsLink(
8 createClient({
9 url: process.env.NEXT_PUBLIC_WS_URL!,
10 connectionParams: {
11 authToken: localStorage.getItem('token'),
12 },
13 })
14);
15
16const splitLink = split(
17 ({ query }) => {
18 const definition = getMainDefinition(query);
19 return (
20 definition.kind === 'OperationDefinition' &&
21 definition.operation === 'subscription'
22 );
23 },
24 wsLink,
25 httpLink
26);
27
28// Subscription hook
29const MESSAGE_SUBSCRIPTION = gql`
30 subscription OnMessage($channelId: ID!) {
31 messageAdded(channelId: $channelId) {
32 id
33 content
34 author {
35 id
36 name
37 }
38 createdAt
39 }
40 }
41`;
42
43function useMessages(channelId: string) {
44 const { data, loading } = useQuery(GET_MESSAGES, {
45 variables: { channelId },
46 });
47
48 useSubscription(MESSAGE_SUBSCRIPTION, {
49 variables: { channelId },
50 onData: ({ client, data }) => {
51 const newMessage = data.data.messageAdded;
52
53 client.cache.modify({
54 id: client.cache.identify({ __typename: 'Channel', id: channelId }),
55 fields: {
56 messages(existing = []) {
57 const newMessageRef = client.cache.writeFragment({
58 data: newMessage,
59 fragment: MESSAGE_FRAGMENT,
60 });
61 return [...existing, newMessageRef];
62 },
63 },
64 });
65 },
66 });
67
68 return { messages: data?.messages ?? [], loading };
69}Fragment Colocation#
1// Colocate fragments with components
2// components/UserAvatar.tsx
3export const USER_AVATAR_FRAGMENT = gql`
4 fragment UserAvatar on User {
5 id
6 name
7 avatar
8 }
9`;
10
11export function UserAvatar({ user }: { user: UserAvatarFragment }) {
12 return <img src={user.avatar} alt={user.name} />;
13}
14
15// Compose fragments in queries
16const GET_POST = gql`
17 ${USER_AVATAR_FRAGMENT}
18
19 query GetPost($id: ID!) {
20 post(id: $id) {
21 id
22 title
23 content
24 author {
25 ...UserAvatar
26 }
27 }
28 }
29`;Best Practices#
Queries:
✓ Use fragments for reusable fields
✓ Colocate queries with components
✓ Use generated types (codegen)
✓ Handle loading and error states
Mutations:
✓ Use optimistic updates for UX
✓ Update cache properly
✓ Handle errors gracefully
✓ Show loading feedback
Caching:
✓ Configure type policies
✓ Use cache.modify for updates
✓ Normalize data with keyFields
✓ Handle pagination correctly
Conclusion#
Apollo Client provides a complete GraphQL solution for React. Master queries and mutations, leverage the cache effectively with type policies, and use optimistic updates for responsive UIs. Colocate fragments with components for maintainability and use subscriptions for real-time features.