Claude-skill-registry apollo-caching-strategies
Use when implementing Apollo caching strategies including cache policies, optimistic UI, cache updates, and normalization.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/apollo-caching-strategies" ~/.claude/skills/majiayu000-claude-skill-registry-apollo-caching-strategies && rm -rf "$T"
manifest:
skills/data/apollo-caching-strategies/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
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
1. 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> ); }
2. 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; } } }); }
3. 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} />; }
4. 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>; }
5. 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(); } } } } } });
6. 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() } } } });
7. 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; } } } } } });
8. 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> ); }
9. 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; } } } } } });
10. 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