Apollo Caching Strategies

VerifiedSafe

Implement Apollo Client caching strategies including cache policies, optimistic UI, cache updates, and normalization.

Sby Skills Guide Bot
DevelopmentIntermediate
3009/3/2026
Claude Code
#apollo-client#graphql#caching#react#state-management

Recommended for

Our review

Guides implementation of Apollo Client caching strategies, including cache configuration, fetch policies, pagination merges, and cache read/write operations.

Strengths

  • Shows each Apollo fetch policy in a concrete React/useQuery example, making trade-offs easy to compare.
  • Explains normalized cache setup with typePolicies, keyFields, merge functions, and computed read fields.
  • Includes practical cache read/write helpers and patterns for keeping the cache in sync after changes.
  • Focused on Apollo Client-specific APIs rather than generic GraphQL advice.

Limitations

  • Examples assume Apollo Client v3 and React; patterns may need adaptation for Apollo Client v4 or non-React frameworks.
  • The provided excerpt cuts off mid-code and does not fully cover optimistic UI, eviction, or testing.
  • Covers client-side caching only, not server-side GraphQL or CDN caching.
When to use it

Use when you need to configure or debug Apollo Client cache behavior such as pagination, normalization, fetch policies, or post-mutation cache updates.

When not to use it

Avoid when you are using a different GraphQL client (Relay, urql, React Query) or need server-side/GraphQL response caching guidance.

Security analysis

Safe
Quality score90/100

The skill contains only educational code examples for Apollo Client caching strategies. It does not instruct any destructive, exfiltrating, or obfuscated actions. Bash is declared as an allowed tool but is not used in any risky way within the provided content.

No concerns found

Examples

Offset pagination merge
In my Apollo Client app, the `posts` query replaces the previous page when I fetch page 2. Update my `InMemoryCache` type policy so offset-based pagination merges incoming posts into the existing list while keeping the `filter` argument as the key.
Update cache after mutation
After my `createPost` mutation succeeds, the new post doesn't appear in the cached `posts` list. Write the Apollo cache update logic (using `client.modify` or `writeQuery`) to append the returned post for the current filter and offset.
Computed normalized fields
Configure Apollo Client so `User` entities are normalized by `email`, and add a computed `fullName` field that reads `firstName` and `lastName` from the cache.

name: apollo-caching-strategies description: Use when implementing Apollo caching strategies including cache policies, optimistic UI, cache updates, and normalization. allowed-tools:

  • Read
  • Write
  • Edit
  • Grep
  • Glob
  • Bash

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

  1. Choose appropriate fetch policies - Match policy to data freshness needs
  2. Use optimistic updates - Improve perceived performance
  3. Normalize cache properly - Configure keyFields correctly
  4. Implement pagination - Handle large datasets efficiently
  5. Persist critical data - Cache auth state and user preferences
  6. Monitor cache size - Prevent memory bloat
  7. Use reactive variables - Manage local state efficiently
  8. Warm cache strategically - Prefetch critical data
  9. Evict unused data - Clean up after deletions
  10. Debug cache issues - Use Apollo DevTools effectively

Common Pitfalls

  1. Wrong fetch policy - Using cache-first for real-time data
  2. Cache denormalization - Missing or incorrect keyFields
  3. Memory leaks - Not evicting deleted items
  4. Over-caching - Caching too much data
  5. Stale data - Not invalidating cache properly
  6. Missing updates - Forgetting to update cache after mutations
  7. Incorrect merges - Wrong pagination merge logic
  8. Cache thrashing - Too many cache writes
  9. Persistence issues - Storing sensitive data
  10. 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

Related skills