Back to Blog
GraphQLApolloReactAPI

GraphQL Client Patterns with Apollo

Master GraphQL client development. From queries and mutations to caching to optimistic updates and error handling.

B
Bootspring Team
Engineering
December 10, 2021
7 min read

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.

Share this article

Help spread the word about Bootspring