Research exploring Apollo Client's sync-oriented architecture versus traditional refetch patterns in GraphQL state management
Apollo Client operates primarily on a synchronization model rather than a “post-and-re-download” pattern. This fundamental difference sets it apart from other data fetching approaches.
1. Normalized Cache as Single Source of Truth Apollo maintains a normalized cache that acts as a local replica of your server state:
// When you fetch data, it gets normalized into the cache
const { data } = useQuery(GET_USER, { variables: { id: 1 } });
// Cache structure after normalization:
{
"User:1": {
"id": 1,
"name": "John",
"__typename": "User"
},
"ROOT_QUERY": {
"user({id:1})": { "__ref": "User:1" }
}
}
2. Mutations Update Cache Directly When you perform mutations, Apollo synchronizes the cache rather than refetching:
const [updateUser] = useMutation(UPDATE_USER, {
update(cache, { data: { updateUser } }) {
// Direct cache synchronization - no refetch needed
cache.modify({
id: cache.identify(updateUser),
fields: {
name() { return updateUser.name; }
}
});
}
});
Strategy | When Used | How It Works |
---|---|---|
Automatic Sync | Entity mutations with IDs | Apollo automatically updates normalized entities |
Manual Cache Updates | List operations, complex changes | Use update function to modify cache directly |
Optimistic Sync | Immediate UI feedback | Update cache immediately, reconcile with server later |
// ✅ Sync pattern - Apollo's strength
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data: { addTodo } }) {
// Synchronize cache with new data
const { todos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: [...todos, addTodo] }
});
}
});
// Result: Instant UI update, no network request
// ⚠️ Refetch pattern - works but defeats Apollo's purpose
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }] // Forces new network request
});
// Result: Network delay, but simpler logic
Happens automatically when:
id
and __typename
// This automatically syncs - no update function needed
const UPDATE_USER_NAME = gql`
mutation UpdateUserName($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id # ✅ Required for auto-sync
name # ✅ Will automatically update in cache
__typename # ✅ Added automatically
}
}
`;
Required for:
const [addComment] = useMutation(ADD_COMMENT, {
update(cache, { data: { addComment } }) {
// Manual sync: add comment to post's comment list
cache.modify({
id: cache.identify({ __typename: 'Post', id: postId }),
fields: {
comments(existingComments = []) {
const newCommentRef = cache.writeFragment({
data: addComment,
fragment: COMMENT_FRAGMENT
});
return [...existingComments, newCommentRef];
}
}
});
}
});
For instant UI feedback:
const [likePost] = useMutation(LIKE_POST, {
optimisticResponse: {
likePost: {
__typename: 'Post',
id: postId,
likesCount: currentLikes + 1, // Optimistic update
isLiked: true
}
}
// Cache syncs immediately with optimistic data
// Then reconciles with real server response
});
✅ Instant UI Updates: No network round-trip delay ✅ Efficient: Avoids unnecessary data transfer ✅ Consistent: Single source of truth in cache ✅ Automatic Propagation: Updates appear everywhere the data is used
⚠️ Complex server-side calculations that can’t be predicted client-side ⚠️ Simple applications where sync complexity isn’t worth it ⚠️ Error scenarios to ensure consistency
// Sometimes refetch is simpler
const [complexCalculation] = useMutation(COMPLEX_MUTATION, {
refetchQueries: [{ query: GET_DASHBOARD_STATS }]
// Easier than trying to manually calculate new stats
});
Action → Server → Re-fetch Data → Update UI
Action → Sync Cache → Update UI → (Server confirms in background)
// Like a post - instant feedback
const [likePost] = useMutation(LIKE_POST, {
optimisticResponse: {
likePost: { id: postId, isLiked: true, likesCount: likes + 1 }
}
// UI updates instantly, all components using this post sync automatically
});
// Like a post - wait for server
const [likePost] = useMutation(LIKE_POST, {
refetchQueries: [GET_POSTS, GET_POST_DETAILS]
// UI waits for network, multiple queries refetch
});
Apollo Client is fundamentally designed around cache synchronization rather than refetching. You’re interacting with the server through:
This sync-oriented approach is Apollo’s superpower - it makes apps feel instant while maintaining consistency with the server. The refetch approach works but doesn’t leverage Apollo’s core strengths and can make your app feel slower and less efficient.
Apollo GraphQL Mutations Documentation
https://www.apollographql.com/docs/react/data/mutations
Keeping Apollo Cache Up to Date After Mutations
https://www.splitgraph.com/blog/keeping-apollo-cache-up-to-date-after-mutations
Apollo Client Caching Overview
https://www.apollographql.com/docs/react/caching/overview
How to Update the Apollo Client’s Cache After a Mutation
https://www.freecodecamp.org/news/how-to-update-the-apollo-clients-cache-after-a-mutation-79a0df79b840/
Manually Update Apollo Cache After GraphQL Mutation
https://curiosum.com/til/manually-update-apollo-cache-after-graphql-mutation
Understanding Apollo Client Cache: Nested Data Structures
https://dev.to/bhanufyi/understanding-apollo-client-cache-how-to-manage-and-update-nested-data-structures-effectively-11n8
Optimistic Updates in Apollo Client React
https://www.linkedin.com/pulse/optimistic-updation-apollo-client-react-antematter-5wthc
Apollo Client Refetching Documentation
https://www.apollographql.com/docs/react/data/refetching
GraphQL Caching and Micro Frontends
https://adamrackis.dev/blog/graphql-caching-and-micro
Best Practice Libraries for Maintaining Local State
https://www.reddit.com/r/webdev/comments/vz6nq2/best_practice_libraries_for_maintaining_local/