Apollo Client GraphQL Caching

Apollo Client’s InMemoryCache normalizes GraphQL responses into an entity store keyed by __typename and a stable id, so a field updated in one query updates everywhere it’s referenced. GraphQL already trims over-fetching at the query layer; the client cache is what stops repeated requests and keeps UI state consistent across routes. It’s one tier in the broader Data Fetching & Caching Strategies stack.

Normalized Cache Architecture

Apollo doesn’t store raw JSON trees. It flattens nested responses into a flat map keyed by __typename plus a stable identifier (id or _id). When two components request overlapping data, Apollo resolves references from this store instead of hitting the network — no duplicate state, consistent UI across route transitions, smaller payloads on client-side navigation.

The constraint is identifier discipline. Every cacheable type needs a stable, unique key. If a CMS returns UUIDs on one endpoint and slugs on another for the same node, Apollo fragments the cache and treats identical content as separate entities. Fix it by setting keyFields to a CMS-native identifier (slug, externalId) or by normalizing payloads at the GraphQL gateway before they reach the browser.

How a query resolves against the normalized store:

flowchart TD
  Q["Component query"] --> L{"Refs in store?"}
  L -->|hit| R["Resolve from entity store"]
  L -->|miss| N["Network fetch"]
  N --> F["Flatten by __typename + keyFields"]
  F --> S["Entity store (flat map)"]
  S --> R
  R --> U["Render"]
  S -.->|"shared entity updates everywhere"| U

Configuration & Type Policies

typePolicies is the control plane for how content types are identified, merged, and read. The cache configuration docs cover the full API surface.

JavaScript
import { InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache({
  typePolicies: {
    Article: {
      keyFields: ['slug'],
    },
    Author: {
      keyFields: ['email'],
    },
    Category: {
      fields: {
        articles: {
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          },
          read(existing = []) {
            return existing;
          },
        },
      },
    },
  },
});

The merge function handles cursor pagination and infinite-scroll lists without full refetches. For REST-heavy or document-oriented backends where normalization buys you little, React Query for CMS Data offers a simpler key-based model.

Cache Updates & Optimistic UI

Reads from the cache are automatic; writes are not. Use cache.modify for field-level updates and cache.writeQuery for full document replacements. When an editor publishes a draft, an optimistic update reflects the change instantly while the mutation resolves in the background — see optimistic UI patterns. In editorial workflows, perceived latency is authoring velocity.

JavaScript
// Optimistic update for publishing an article
client.mutate({
  mutation: PUBLISH_ARTICLE,
  variables: { slug: 'headless-cms-patterns' },
  optimisticResponse: {
    publishArticle: { __typename: 'Article', slug: 'headless-cms-patterns', status: 'published' },
  },
  update(cache, { data }) {
    cache.modify({
      id: cache.identify({ __typename: 'Article', slug: 'headless-cms-patterns' }),
      fields: {
        status: () => data.publishArticle.status,
      },
    });
  },
});

If the API rejects the mutation, Apollo reverts the cache to its pre-mutation state automatically — but you still own the rollback logic for any derived UI. For workflows that favor background revalidation over optimistic writes, SWR Stale-While-Revalidate Patterns trade eventual consistency for less client-side state.

Production Hardening & Debugging

Unbounded cache growth degrades memory over long sessions. Set maxSize and pick fetch policies deliberately — cache-and-network to balance freshness against bandwidth. Inspect the live cache with Apollo DevTools and the @apollo/client/cache introspection APIs to verify normalization paths and find orphaned entities.

Wire cache validation into CI: snapshot tests against normalized cache state catch the case where a CMS schema migration or field deprecation silently breaks client hydration.

Implementation Checklist

  • Enforce stable __typename and id/keyFields alignment across all CMS content types.
  • Implement merge and read policies for all list and pagination fields.
  • Use cache.modify for targeted updates; reserve writeQuery for full-tree replacements.
  • Configure fetchPolicy: 'cache-first' for static content and cache-and-network for dynamic editorial previews.
  • Monitor cache hit rates and memory allocation via Apollo DevTools in staging environments.