Apollo Client Advanced
Advanced Apollo Client Techniques
Beyond basic queries, Apollo Client offers powerful features for mutations, cache management, optimistic UI, lazy queries, subscriptions, and local state. Master these techniques to build highly responsive and efficient GraphQL applications.
useMutation Hook
The useMutation hook executes GraphQL mutations to create, update, or delete data.
import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
}
}
`;
function CreatePostForm() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [createPost, { data, loading, error }] = useMutation(CREATE_POST);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createPost({
variables: { title, content }
});
// Clear form on success
setTitle('');
setContent('');
alert('Post created successfully!');
} catch (err) {
console.error('Mutation error:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
export default CreatePostForm;
Cache Updates After Mutations
After a mutation, you need to update the Apollo cache so the UI reflects the changes without refetching.
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
}
}
`;
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
content
}
}
`;
function CreatePostForm() {
const [createPost] = useMutation(CREATE_POST, {
update(cache, { data: { createPost } }) {
// Read existing posts from cache
const existingPosts = cache.readQuery({ query: GET_POSTS });
// Write updated posts back to cache
cache.writeQuery({
query: GET_POSTS,
data: {
posts: [...existingPosts.posts, createPost]
}
});
}
});
// Form implementation...
}
const [deletePost] = useMutation(DELETE_POST, {
refetchQueries: [
{ query: GET_POSTS },
{ query: GET_USER_POSTS, variables: { userId: user.id } }
]
});
// Or use refetchQueries callback
const [updatePost] = useMutation(UPDATE_POST, {
refetchQueries: (result) => [
{ query: GET_POST, variables: { id: result.data.updatePost.id } }
]
});
Optimistic UI
Optimistic UI updates the cache immediately (before server response) to make the app feel instant. If the mutation fails, Apollo reverts the change.
import { useMutation, gql } from '@apollo/client';
const LIKE_POST = gql`
mutation LikePost($postId: ID!) {
likePost(postId: $postId) {
id
likes
isLiked
}
}
`;
function LikeButton({ post }) {
const [likePost] = useMutation(LIKE_POST, {
optimisticResponse: {
likePost: {
__typename: 'Post',
id: post.id,
likes: post.likes + 1,
isLiked: true
}
}
});
return (
<button onClick={() => likePost({ variables: { postId: post.id } })}>
{post.isLiked ? 'Unlike' : 'Like'} ({post.likes})
</button>
);
}
- Use for fast, low-risk operations (likes, follows, toggles)
- Keep optimistic response structure identical to actual response
- Include
__typenamein optimistic response for cache normalization - Avoid for critical operations (payments, data deletion)
useLazyQuery Hook
Unlike useQuery which executes immediately, useLazyQuery returns a function you can call manually when needed.
import React, { useState } from 'react';
import { useLazyQuery, gql } from '@apollo/client';
const SEARCH_USERS = gql`
query SearchUsers($query: String!) {
searchUsers(query: $query) {
id
name
email
avatar
}
}
`;
function UserSearch() {
const [query, setQuery] = useState('');
// Returns [executeQuery, { data, loading, error }]
const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);
const handleSearch = (e) => {
e.preventDefault();
searchUsers({ variables: { query } });
};
return (
<div>
<form onSubmit={handleSearch}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users..."
/>
<button type="submit">Search</button>
</form>
{loading && <p>Searching...</p>}
{error && <p>Error: {error.message}</p>}
{data && (
<ul>
{data.searchUsers.map(user => (
<li key={user.id}>
<img src={user.avatar} alt={user.name} />
{user.name} - {user.email}
</li>
))}
</ul>
)}
</div>
);
}
export default UserSearch;
useSubscription Hook
The useSubscription hook subscribes to real-time updates via WebSocket.
import React from 'react';
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
content
author {
id
name
avatar
}
createdAt
}
}
`;
function ChatMessages({ channelId, messages }) {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION, {
variables: { channelId },
onData: ({ client, data }) => {
// Handle new message
console.log('New message:', data.data.messageAdded);
}
});
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.author.name}:</strong> {msg.content}
</div>
))}
{loading && <p>Connecting to chat...</p>}
</div>
);
}
export default ChatMessages;
Local State Management
Apollo Client can manage local state alongside remote data, eliminating the need for separate state management libraries.
import { makeVar, useReactiveVar } from '@apollo/client';
// Create reactive variable
export const isLoggedInVar = makeVar(false);
export const currentUserVar = makeVar(null);
export const themeVar = makeVar('light');
// Use in components
function Header() {
const isLoggedIn = useReactiveVar(isLoggedInVar);
const currentUser = useReactiveVar(currentUserVar);
const theme = useReactiveVar(themeVar);
const toggleTheme = () => {
themeVar(theme === 'light' ? 'dark' : 'light');
};
return (
<header className={theme}>
{isLoggedIn ? (
<div>Welcome, {currentUser.name}</div>
) : (
<button>Log In</button>
)}
<button onClick={toggleTheme}>
Toggle Theme
</button>
</header>
);
}
import { isLoggedInVar, currentUserVar } from './cache';
// Read current value
const isLoggedIn = isLoggedInVar();
// Update value
isLoggedInVar(true);
currentUserVar({ id: '1', name: 'John Doe', email: 'john@example.com' });
// In mutations
const [login] = useMutation(LOGIN, {
onCompleted: (data) => {
isLoggedInVar(true);
currentUserVar(data.login.user);
}
});
const [logout] = useMutation(LOGOUT, {
onCompleted: () => {
isLoggedInVar(false);
currentUserVar(null);
}
});
Cache Manipulation
Directly read, write, and modify the Apollo cache for advanced use cases.
import { useApolloClient } from '@apollo/client';
function UserProfile({ userId }) {
const client = useApolloClient();
// Read from cache
const cachedUser = client.readFragment({
id: `User:${userId}`,
fragment: gql`
fragment UserData on User {
id
name
email
}
`
});
// Write to cache
const updateUserInCache = (updates) => {
client.writeFragment({
id: `User:${userId}`,
fragment: gql`
fragment UserData on User {
id
name
email
}
`,
data: {
...cachedUser,
...updates
}
});
};
// Evict from cache (remove)
const removeUserFromCache = () => {
client.cache.evict({ id: `User:${userId}` });
client.cache.gc(); // Garbage collect
};
return (
<div>
{/* Component UI */}
</div>
);
}
Error Handling in Mutations
const [updateUser, { loading, error }] = useMutation(UPDATE_USER, {
onError: (error) => {
if (error.graphQLErrors) {
error.graphQLErrors.forEach(({ message, extensions }) => {
if (extensions?.code === 'UNAUTHENTICATED') {
// Redirect to login
window.location.href = '/login';
} else if (extensions?.code === 'BAD_USER_INPUT') {
// Show validation errors
alert(`Validation error: ${message}`);
}
});
}
if (error.networkError) {
alert('Network error. Please try again.');
}
},
onCompleted: (data) => {
alert('User updated successfully!');
}
});
- Create a mutation to add a comment to a post with useMutation
- Implement manual cache update to add the new comment to the post's comment list
- Add optimistic UI to instantly show the comment before server confirmation
- Build a search feature using useLazyQuery that only executes when the user submits a form
- Create reactive variables for dark mode preference and use them across multiple components