Optimistic UI
Optimistic updates in GraphQL clients: predict the mutation result locally so UI feels instant; reconcile when the server responds.
GraphQL — optimistic updates
EXAMPLE
// ===== Why optimistic =====
// Mutations have network latency. Without optimistic updates, the UI flickers
// while waiting. With them, the UI changes immediately; if the server fails, we roll back.
// ===== Apollo Client =====
import { useMutation, gql } from '@apollo/client';
const ADD_TODO = gql\`
mutation Add($text: String!) {
addTodo(text: $text) {
id text done
}
}
\`;
const [addTodo] = useMutation(ADD_TODO, {
optimisticResponse: (vars) => ({
addTodo: {
__typename: 'Todo',
id: 'temp-' + Date.now(),
text: vars.text,
done: false,
},
}),
update: (cache, { data }) => {
cache.modify({
fields: {
todos(existing = []) {
const newRef = cache.writeFragment({
data: data.addTodo,
fragment: gql\`fragment Todo on Todo { id text done }\`,
});
return [...existing, newRef];
},
},
});
},
});
// ===== urql (graphCache) =====
// npm install @urql/exchange-graphcache
import { cacheExchange } from '@urql/exchange-graphcache';
const cache = cacheExchange({
optimistic: {
addTodo: (args, cache, info) => ({
__typename: 'Todo',
id: 'temp-' + Date.now(),
text: args.text,
done: false,
}),
},
updates: {
Mutation: {
addTodo: (result, args, cache) => {
cache.invalidate('Query', 'todos');
},
},
},
});
// ===== Relay =====
// Uses updater functions on commitMutation:
commitMutation(environment, {
mutation: AddTodoMutation,
variables: { text },
optimisticUpdater: (store) => {
const todo = store.create('temp-' + Date.now(), 'Todo');
todo.setValue(text, 'text');
todo.setValue(false, 'done');
const root = store.getRoot();
const todos = root.getLinkedRecords('todos') || [];
root.setLinkedRecords([...todos, todo], 'todos');
},
updater: (store) => { /* real update from server response */ },
});
// ===== Pattern: rollback on error =====
// Apollo + urql roll back automatically if the mutation errors.
// Show a toast: 'Failed to add'; the optimistic record reverts.
// ===== Caveats =====
// - Server may return a different shape than predicted -> reconcile carefully
// - Generated IDs (UUIDs) on client -> sometimes the server returns a different ID
// - Realtime subscriptions add 'real' record before mutation returns -> dedup
// ===== When optimistic helps =====
// - Toggles (like, follow, mark-as-read)
// - Add to list (todo, comment)
// - Reordering / drag-and-drop
// - Form save in a stable UI
// ===== When to skip =====
// - Mutations that change critical balances (payment) -> wait
// - Mutations the user must see fail before retrying (validation-heavy)
// - Cross-entity coordinated changes (saga-style)
// ===== Patterns =====
// - Predict the EXPECTED shape with __typename
// - Use cache.modify / cache.writeFragment in Apollo
// - Show pending state subtly (spinner, opacity)
// - Toast on error; auto-rollback handles UI
// ===== Pitfalls =====
// - Optimistic with subscriptions -> double records
// - Server returning different IDs without remapping
// - Optimistic without rollback -> stale UI on error
// - Predicting too much (status changes that depend on server-side logic)
Why it matters
Optimistic updates make slow networks feel fast. Apollo cache.modify, urql graphCache optimistic, Relay optimisticUpdater. Predict the shape with __typename, reconcile when the server responds, roll back on error. Toggles + add-to-list are the obvious wins; payments and saga-style flows are not.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
client.mutate({ mutation, optimisticResponse: { ... }, update(cache) { ... } });
Try it Yourself »
Discussion
Loading…