Stopping Redundant GraphQL Fetches with Apollo Client Normalization
Stop redundant network requests in your GraphQL app. Learn how Apollo Client's normalized cache flattens data and how to manage state updates without full page reloads.
25 Feb 2026, 21:13 UTC

The Problem: The "Stale Data" and "Double Fetch" Loop
In a typical GraphQL application, you often fetch the same object in different views. For example, a user's profile might be requested in a header component and again in a detailed settings page. Without a strategy for state management, the application performs a network request for every single component that needs that data, even if the data was fetched milliseconds ago.
The takeaway is simple: To stop redundant network traffic and ensure UI consistency, you must leverage Apollo Client's Normalized Cache. Instead of storing raw JSON responses, the cache flattens your data into a lookup table, allowing different queries to share the same source of truth.
How Normalization Flattens Your Data
When Apollo Client receives a GraphQL response, it doesn't just save the result as a blob. It performs a process called normalization. It looks for a unique identifier—usually id or _id—and creates a unique key for that object (e.g., User:123). Any nested objects are stripped out and stored as their own entries, with the parent object holding a reference to the child's ID.
This means if Query A fetches a User and their Posts, and Query B later fetches just that same User, Apollo recognizes the User:123 key. If the fetchPolicy is set to cache-first (the default), Apollo resolves the request locally without ever hitting the network.
Implementing a Cache-Aware Mutation
The biggest challenge with a normalized cache occurs during mutations. If you update a user's name via a mutation, the server returns the updated object. Because the ID matches the existing entry in the cache, Apollo updates the name globally. However, if your mutation adds a new item to a list, Apollo doesn't know which lists that new item belongs to.
To handle this, you use the update function to manually modify the cache. Here is a configuration for adding a new comment to a post:
// Run this in your frontend component using useMutation
const [addComment] = useMutation(ADD_COMMENT_MUTATION, {
update(cache, { data: { addComment } }) {
// Read the existing posts query from the cache
const existingPosts = cache.readQuery({ query: GET_POSTS });
if (existingPosts) {
// Write back to the cache with the new comment appended
cache.writeQuery({
query: GET_POSTS,
data: {
posts: [
...existingPosts.posts,
addComment,
],
},
});
}
},
});
Execution Details
- Permissions: Client-side execution; requires the
ApolloClientinstance to be provided viaApolloProvider. - Placeholders:
GET_POSTSandADD_COMMENT_MUTATIONshould be your definedgqltemplate literals. - Expected Result: The UI should update immediately to show the new comment without a page refresh or a
refetchQueriescall. - Risk: If
GET_POSTSis not currently in the cache,readQuerywill returnnull. Always include a null check to avoid runtime crashes.
Trade-offs and Limitations
Normalization is powerful, but it introduces specific overheads:
- ID Dependency: If your GraphQL schema lacks consistent IDs or you forget to request the
idfield in a query, Apollo cannot normalize the object. This results in a "cache miss," forcing a network request every time. - Memory Footprint: In very large applications with thousands of cached entities, the client-side memory usage can grow. You may need to implement
cache.evict()to remove stale data. - Complexity: Manual cache updates (using
writeQueryorwriteFragment) can become verbose and error-prone as the data graph grows.
Verifying Your Cache State
To ensure your normalization is working and you aren't accidentally triggering redundant requests, use these two verification steps:
- Apollo Client DevTools: Install the browser extension to inspect the "Cache" tab. You should see a flat list of objects (e.g.,
Post:1,User:5) rather than nested query results. - Network Tab: Open your browser's Network inspector. Trigger a query, navigate away, and return. If the second visit does not trigger a new XHR/Fetch request for the same data, the
cache-firstpolicy is functioning correctly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.