Apollo Caching Strategies
Master Apollo Client's caching mechanisms for building performant applications with optimal data fetching and state management strategies.
Overview
Apollo Client's intelligent cache is a normalized, in-memory data store that allows for efficient data fetching and updates. Understanding cache policies and management strategies is crucial for building high-performance apps.
Installation and Setup
Cache Configuration
// apollo/cache.js import { InMemoryCache, makeVar } from '@apollo/client';
export const cache = new InMemoryCache({ typePolicies: { Query: { fields: { posts: { // Pagination with offset keyArgs: ['filter'], merge(existing = [], incoming, { args }) { const merged = existing.slice(0); const offset = args?.offset || 0;
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
}
}
}
},
Post: {
keyFields: ['id'],
fields: {
comments: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
},
User: {
keyFields: ['email'],
fields: {
fullName: {
read(_, { readField }) {
return `${readField('firstName')} ${readField('lastName')}`;
}
}
}
}
} });
Core Patterns
- Fetch Policies
// Different fetch policies for different use cases import { useQuery } from '@apollo/client'; import { GET_POSTS } from './queries';
// cache-first (default): Check cache first, network if not found function CacheFirstPosts() { const { data } = useQuery(GET_POSTS, { fetchPolicy: 'cache-first' }); return <PostsList posts={data?.posts} />; }
// cache-only: Never make network request, cache or error function CacheOnlyPosts() { const { data } = useQuery(GET_POSTS, { fetchPolicy: 'cache-only' }); return <PostsList posts={data?.posts} />; }
// cache-and-network: Return cache immediately, update with network function CacheAndNetworkPosts() { const { data, loading, networkStatus } = useQuery(GET_POSTS, { fetchPolicy: 'cache-and-network', notifyOnNetworkStatusChange: true });
return ( <div> {networkStatus === 1 && <Spinner />} <PostsList posts={data?.posts} /> </div> ); }
// network-only: Always make network request, update cache function NetworkOnlyPosts() { const { data } = useQuery(GET_POSTS, { fetchPolicy: 'network-only' }); return <PostsList posts={data?.posts} />; }
// no-cache: Always make network request, don't update cache function NoCachePosts() { const { data } = useQuery(GET_POSTS, { fetchPolicy: 'no-cache' }); return <PostsList posts={data?.posts} />; }
// standby: Like cache-first but doesn't auto-update function StandbyPosts() { const { data, refetch } = useQuery(GET_POSTS, { fetchPolicy: 'standby' });
return ( <div> <button onClick={() => refetch()}>Refresh</button> <PostsList posts={data?.posts} /> </div> ); }
- Cache Reads and Writes
// apollo/cacheOperations.js import { gql } from '@apollo/client';
// Read from cache
export function readPostFromCache(client, postId) {
try {
const data = client.readQuery({
query: gql query GetPost($id: ID!) { post(id: $id) { id title body } } ,
variables: { id: postId }
});
return data?.post;
} catch (error) {
console.error('Post not in cache:', error);
return null;
}
}
// Write to cache
export function writePostToCache(client, post) {
client.writeQuery({
query: gql query GetPost($id: ID!) { post(id: $id) { id title body } } ,
variables: { id: post.id },
data: { post }
});
}
// Read fragment
export function readPostFragment(client, postId) {
return client.readFragment({
id: Post:${postId},
fragment: gql fragment PostFields on Post { id title body likesCount }
});
}
// Write fragment
export function updatePostLikes(client, postId, likesCount) {
client.writeFragment({
id: Post:${postId},
fragment: gql fragment PostLikes on Post { likesCount } ,
data: {
likesCount
}
});
}
// Modify cache fields export function incrementPostLikes(client, postId) { client.cache.modify({ id: client.cache.identify({ __typename: 'Post', id: postId }), fields: { likesCount(currentCount = 0) { return currentCount + 1; }, isLiked() { return true; } } }); }
- Optimistic Updates
// components/OptimisticLike.js import { useMutation } from '@apollo/client'; import { LIKE_POST } from '../mutations';
function OptimisticLike({ post }) { const [likePost] = useMutation(LIKE_POST, { variables: { postId: post.id },
// Optimistic response
optimisticResponse: {
__typename: 'Mutation',
likePost: {
__typename: 'Post',
id: post.id,
likesCount: post.likesCount + 1,
isLiked: true
}
},
// Update cache
update(cache, { data: { likePost } }) {
cache.modify({
id: cache.identify(post),
fields: {
likesCount() {
return likePost.likesCount;
},
isLiked() {
return likePost.isLiked;
}
}
});
},
// Handle errors
onError(error) {
console.error('Like failed, reverting:', error);
// Optimistic update automatically reverted
}
});
return ( <button onClick={() => likePost()}> {post.isLiked ? 'Unlike' : 'Like'} ({post.likesCount}) </button> ); }
// Complex optimistic update with multiple changes
function OptimisticCreateComment({ postId }) {
const [createComment] = useMutation(CREATE_COMMENT, {
optimisticResponse: ({ body }) => ({
__typename: 'Mutation',
createComment: {
__typename: 'Comment',
id: temp-${Date.now()},
body,
createdAt: new Date().toISOString(),
author: {
__typename: 'User',
id: currentUser.id,
name: currentUser.name,
avatar: currentUser.avatar
}
}
}),
update(cache, { data: { createComment } }) {
// Add comment to post
cache.modify({
id: cache.identify({ __typename: 'Post', id: postId }),
fields: {
comments(existing = []) {
const newCommentRef = cache.writeFragment({
data: createComment,
fragment: gql`
fragment NewComment on Comment {
id
body
createdAt
author {
id
name
avatar
}
}
`
});
return [...existing, newCommentRef];
},
commentsCount(count = 0) {
return count + 1;
}
}
});
}
});
return <CommentForm onSubmit={createComment} />; }
- Cache Eviction
// apollo/eviction.js export function evictPost(client, postId) { // Evict specific post client.cache.evict({ id: client.cache.identify({ __typename: 'Post', id: postId }) });
// Garbage collect client.cache.gc(); }
export function evictField(client, postId, fieldName) { // Evict specific field client.cache.evict({ id: client.cache.identify({ __typename: 'Post', id: postId }), fieldName }); }
export function evictAllPosts(client) { // Evict all posts from cache client.cache.modify({ fields: { posts(existing, { DELETE }) { return DELETE; } } });
client.cache.gc(); }
// Usage in delete mutation function DeletePost({ postId }) { const [deletePost] = useMutation(DELETE_POST, { variables: { id: postId },
update(cache) {
// Remove from posts list
cache.modify({
fields: {
posts(existingPosts = [], { readField }) {
return existingPosts.filter(
ref => postId !== readField('id', ref)
);
}
}
});
// Evict post and related data
cache.evict({ id: cache.identify({ __typename: 'Post', id: postId }) });
cache.gc();
}
});
return <button onClick={() => deletePost()}>Delete</button>; }
- Reactive Variables
// apollo/reactiveVars.js import { makeVar, useReactiveVar } from '@apollo/client';
// Create reactive variables export const cartItemsVar = makeVar([]); export const themeVar = makeVar('light'); export const isModalOpenVar = makeVar(false); export const notificationsVar = makeVar([]);
// Helper functions export function addToCart(item) { const cart = cartItemsVar(); cartItemsVar([...cart, item]); }
export function removeFromCart(itemId) { const cart = cartItemsVar(); cartItemsVar(cart.filter(item => item.id !== itemId)); }
export function clearCart() { cartItemsVar([]); }
export function toggleTheme() { const current = themeVar(); themeVar(current === 'light' ? 'dark' : 'light'); }
export function addNotification(notification) { const notifications = notificationsVar(); notificationsVar([...notifications, { id: Date.now(), ...notification }]); }
// React component usage function Cart() { const cartItems = useReactiveVar(cartItemsVar);
return ( <div> <h2>Cart ({cartItems.length})</h2> {cartItems.map(item => ( <div key={item.id}> {item.name} <button onClick={() => removeFromCart(item.id)}>Remove</button> </div> ))} </div> ); }
// Use in cache configuration const cache = new InMemoryCache({ typePolicies: { Query: { fields: { cartItems: { read() { return cartItemsVar(); } }, theme: { read() { return themeVar(); } } } } } });
- Pagination Strategies
// Offset-based pagination
const POSTS_QUERY = gql query GetPosts($limit: Int!, $offset: Int!) { posts(limit: $limit, offset: $offset) { id title body } };
function OffsetPagination() { const { data, fetchMore } = useQuery(POSTS_QUERY, { variables: { limit: 10, offset: 0 } });
return ( <div> <PostsList posts={data?.posts} /> <button onClick={() => fetchMore({ variables: { offset: data.posts.length } }) } > Load More </button> </div> ); }
// Cursor-based pagination
const CURSOR_POSTS_QUERY = gql query GetPosts($first: Int!, $after: String) { posts(first: $first, after: $after) { edges { cursor node { id title body } } pageInfo { hasNextPage endCursor } } };
function CursorPagination() { const { data, fetchMore } = useQuery(CURSOR_POSTS_QUERY, { variables: { first: 10 } });
return ( <div> {data?.posts.edges.map(({ node }) => ( <Post key={node.id} post={node} /> ))}
{data?.posts.pageInfo.hasNextPage && (
<button
onClick={() =>
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor
}
})
}
>
Load More
</button>
)}
</div>
); }
// Cache configuration for pagination const cache = new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: ['filter'], merge(existing, incoming, { args }) { if (!existing) return incoming;
const { offset = 0 } = args;
const merged = existing.slice(0);
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
}
}
}
}
} });
// Relay-style pagination with offsetLimitPagination import { offsetLimitPagination } from '@apollo/client/utilities';
const cache = new InMemoryCache({ typePolicies: { Query: { fields: { posts: offsetLimitPagination() } } } });
- Cache Persistence
// apollo/persistedCache.js import { InMemoryCache } from '@apollo/client'; import { persistCache, LocalStorageWrapper } from 'apollo3-cache-persist';
export async function createPersistedCache() { const cache = new InMemoryCache({ typePolicies: { // Your type policies } });
await persistCache({ cache, storage: new LocalStorageWrapper(window.localStorage), maxSize: 1048576, // 1 MB debug: true, trigger: 'write', // or 'background' });
return cache; }
// Usage in client setup import { ApolloClient } from '@apollo/client';
async function initApollo() { const cache = await createPersistedCache();
const client = new ApolloClient({ uri: 'http://localhost:4000/graphql', cache });
return client; }
// Clear persisted cache export function clearPersistedCache(client) { client.clearStore(); // Clears cache localStorage.clear(); // Clears persistence }
// Selective persistence const cache = new InMemoryCache({ typePolicies: { User: { fields: { // Don't persist sensitive data authToken: { read() { return null; } } } } } });
- Cache Warming
// apollo/cacheWarming.js import { gql } from '@apollo/client';
export async function warmCache(client) {
// Preload critical queries
await Promise.all([
client.query({
query: gql query GetCurrentUser { me { id name email } }
}),
client.query({
query: gql`
query GetRecentPosts {
posts(limit: 20) {
id
title
excerpt
}
}
`
})
]); }
// Prefetch on hover function PostLink({ postId }) { const client = useApolloClient();
const prefetch = () => { client.query({ query: GET_POST, variables: { id: postId } }); };
return (
<Link
to={/posts/${postId}}
onMouseEnter={prefetch}
onTouchStart={prefetch}
>
View Post
</Link>
);
}
- Cache Redirects
// apollo/cache.js const cache = new InMemoryCache({ typePolicies: { Query: { fields: { post: { read(_, { args, toReference }) { // Redirect to cached object return toReference({ __typename: 'Post', id: args.id }); } } } },
User: {
fields: {
// Computed field from cache
fullName: {
read(_, { readField }) {
const firstName = readField('firstName');
const lastName = readField('lastName');
return `${firstName} ${lastName}`;
}
},
// Field with arguments
posts: {
read(existing, { args, readField }) {
if (args?.published !== undefined) {
return existing?.filter(ref =>
readField('published', ref) === args.published
);
}
return existing;
}
}
}
}
} });
- Cache Monitoring and Debugging
// apollo/monitoring.js export function logCacheContents(client) { const cache = client.extract(); console.log('Cache contents:', cache); }
export function watchCacheChanges(client) {
const observer = client.cache.watch({
query: gql query GetAllData { posts { id title } } ,
callback: (data) => {
console.log('Cache changed:', data);
}
});
return observer; }
// Development helpers if (process.env.NODE_ENV === 'development') { window.apolloClient = client; window.logCache = () => logCacheContents(client);
// Cache size monitoring
setInterval(() => {
const cacheSize = JSON.stringify(client.extract()).length;
console.log(Cache size: ${(cacheSize / 1024).toFixed(2)} KB);
}, 10000);
}
// React DevTools integration import { ApolloClient } from '@apollo/client'; import { ApolloProvider } from '@apollo/client/react';
function App() { return ( <ApolloProvider client={client}> {/* Enable Apollo DevTools */} <YourApp /> </ApolloProvider> ); }
// Custom cache inspector function CacheInspector() { const client = useApolloClient(); const [cacheData, setCacheData] = useState({});
useEffect(() => { const data = client.extract(); setCacheData(data); }, [client]);
return ( <div> <h2>Cache Inspector</h2> <pre>{JSON.stringify(cacheData, null, 2)}</pre> <button onClick={() => client.clearStore()}>Clear Cache</button> </div> ); }
Best Practices
-
Choose appropriate fetch policies - Match policy to data freshness needs
-
Use optimistic updates - Improve perceived performance
-
Normalize cache properly - Configure keyFields correctly
-
Implement pagination - Handle large datasets efficiently
-
Persist critical data - Cache auth state and user preferences
-
Monitor cache size - Prevent memory bloat
-
Use reactive variables - Manage local state efficiently
-
Warm cache strategically - Prefetch critical data
-
Evict unused data - Clean up after deletions
-
Debug cache issues - Use Apollo DevTools effectively
Common Pitfalls
-
Wrong fetch policy - Using cache-first for real-time data
-
Cache denormalization - Missing or incorrect keyFields
-
Memory leaks - Not evicting deleted items
-
Over-caching - Caching too much data
-
Stale data - Not invalidating cache properly
-
Missing updates - Forgetting to update cache after mutations
-
Incorrect merges - Wrong pagination merge logic
-
Cache thrashing - Too many cache writes
-
Persistence issues - Storing sensitive data
-
No error handling - Not handling cache read failures
When to Use
-
Building data-intensive applications
-
Implementing offline-first features
-
Creating real-time collaborative apps
-
Developing mobile applications
-
Building e-commerce platforms
-
Creating social media applications
-
Implementing complex state management
-
Developing admin dashboards
-
Building content management systems
-
Creating analytics applications
Resources
-
Apollo Cache Documentation
-
Advanced Cache Patterns
-
Cache Persistence
-
Apollo DevTools
-
Optimistic UI Guide