Managing Apollo Client Cache Normalization and Local State
Learn how to implement Apollo Client cache normalization and local state management to ensure UI consistency and reduce redundant network requests.
10 Jun 2026, 19:16 UTC

Solving the Stale Data Problem with Normalization
The primary challenge in GraphQL frontend development is ensuring that when a piece of data changes in one part of the UI, every other component displaying that data updates instantly without a full page reload or an expensive network refetch. Apollo Client solves this through Cache Normalization.
Normalization transforms nested GraphQL query responses into a flat lookup table. Instead of storing a tree of data, Apollo extracts objects and stores them by a unique identifier (typically a combination of __typename and id). When a mutation returns an updated object with the same ID, Apollo overwrites the existing entry in the flat table, and every query observing that object triggers a re-render automatically.
How Normalization Works: The Mechanism
By default, Apollo Client uses the InMemoryCache. It scans every object in a response for an id or _id field. If found, it creates a cache key like User:123. If these fields are missing, the object is stored as a "nested" value, meaning updates to that object will not propagate to other queries.
Implementation Example: Customizing Type Policies
While default normalization works for single objects, lists (like paginated search results) require Type Policies to prevent new data from overwriting old data in the cache.
import { ApolloClient, InMemoryCache } = '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Custom merge function for paginated lists
allProducts: {
keyArgs: false, // Ignore arguments like 'offset' to merge all pages into one list
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
Product: {
// Ensure we use a custom ID if 'id' isn't the primary key
keyFields: ['sku'],
},
},
});
const client = new ApolloClient({
uri: 'https://your-api.com/graphql',
cache,
});
Handling Local State with @client
Not all data belongs on the server. UI state—such as whether a sidebar is open or a temporary filter value—can be stored in the Apollo Cache using the @client directive. This allows you to query local state and server state in a single GraphQL request.
Defining Local-Only Fields
To use local state, you must define the field in your cache's type policies so Apollo knows how to resolve it.
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
isSidebarOpen: {
read() {
return this.cache.data.Query.isSidebarOpen || false;
},
},
},
},
},
});
// Updating the local state without a network request
client.cache.writeQuery({
query: GET_SIDEBAR_STATUS,
data: { isSidebarOpen: true },
});
Surgical Updates: writeFragment vs. writeQuery
When a mutation occurs, you have two primary ways to update the cache manually if the automatic normalization isn't sufficient.
- writeQuery: Replaces an entire query result. This is verbose because you must provide the full data structure the query expects.
- writeFragment: Updates a specific object regardless of which queries are using it. This is the preferred method for targeted updates.
Example: Updating a single user's name
client.cache.writeFragment({
id: 'User:123',
fragment: gql`
fragment UpdateUser on User {
name
}
`,
data: {
name: 'New Name',
},
});
Critical Limitations and Common Pitfalls
| Pitfall | Consequence | Solution |
|---|---|---|
Omitting id in selection set |
Object is stored as a nested value; no automatic updates. | Always include id and __typename in queries. |
Incorrect keyArgs in Type Policies |
Cache creates separate entries for every filter/page combination. | Set keyArgs: false or specify which args should create new cache entries. |
| Overusing local state for global data | State is lost on page refresh and diverges across browser tabs. | Use localStorage or a database for persistent global state. |
Verification and Diagnostics
To verify that normalization is functioning correctly, use the Apollo Client DevTools browser extension. Navigate to the Cache tab. If you see a flat list of keys (e.g., Product:1, Product:2) rather than a deeply nested JSON tree, normalization is working.
To test a mutation's effect, perform an update and observe the DevTools. The specific object's value should change instantly, and any UI component using a query that references that ID should re-render without a network call being triggered in the Network tab.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.