Implementation Pattern
Implementation Pattern
Optimistic updates immediately reflect changes in the UI before server confirmation.
React Query Pattern
function TodoApp() {
const queryClient = useQueryClient();
const addTodo = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).then(r => r.json()),
// Optimistic update
onMutate: async (newTodo) => {
// Cancel outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old) => [
...old,
{ ...newTodo, id: 'temp-' + Date.now(), isPending: true },
]);
// Return context object with the snapshotted value
return { previousTodos };
},
// If the mutation fails, use the context we returned to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
toast.error('Failed to add todo');
},
// Always refetch after error or success to ensure consistency
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<div>
<button
onClick={() => addTodo.mutate({ text: 'New Todo', completed: false })}
disabled={addTodo.isPending}
>
Add Todo
</button>
{addTodo.isPending && <span>Saving...</span>}
</div>
);
}
Zustand Pattern
const useStore = create((set, get) => ({
items: [],
pendingItems: [],
addItem: async (item) => {
const tempId = 'temp-' + Date.now();
const optimisticItem = { ...item, id: tempId };
// Optimistic update
set((state) => ({
items: [...state.items, optimisticItem],
pendingItems: [...state.pendingItems, tempId],
}));
try {
const response = await fetch('/api/items', {
method: 'POST',
body: JSON.stringify(item),
});
const savedItem = await response.json();
// Replace temp with real item
set((state) => ({
items: state.items.map(i =>
i.id === tempId ? savedItem : i
),
pendingItems: state.pendingItems.filter(id => id !== tempId),
}));
} catch (error) {
// Rollback
set((state) => ({
items: state.items.filter(i => i.id !== tempId),
pendingItems: state.pendingItems.filter(id => id !== tempId),
}));
toast.error('Failed to add item');
}
},
}));
Key Principles
- Cancel background queries before updating
- Save previous state for rollback
- Update cache optimistically
- Rollback on error
- Invalidate/refetch on settlement
Rollback Handling
Rollback Handling
Properly revert changes when server operations fail.
Rollback Patterns
// Simple rollback
const deleteTodo = useMutation({
mutationFn: (id) =>
fetch(`/api/todos/${id}`, { method: 'DELETE' }),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
// Remove immediately
queryClient.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== id)
);
return { previousTodos };
},
onError: (err, id, context) => {
// Rollback to previous state
queryClient.setQueryData(['todos'], context.previousTodos);
toast.error('Failed to delete todo');
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Partial Rollback
// When you need to keep some changes
const updateTodo = useMutation({
mutationFn: (todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify(todo),
}).then(r => r.json()),
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
// Update with optimistic data
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
)
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Partial rollback: keep client fields, revert server fields
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === newTodo.id
? { ...todo, ...context.previousTodos.find(t => t.id === newTodo.id) }
: todo
)
);
},
onSettled: (data, error, newTodo) => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Rollback with Temp IDs
const addTodo = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text: newTodo.text }),
}).then(r => r.json()),
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
const tempTodo = {
...newTodo,
id: 'temp-' + Date.now(),
createdAt: new Date().toISOString(),
_isPending: true,
};
queryClient.setQueryData(['todos'], (old) => [...old, tempTodo]);
return { previousTodos, tempId: tempTodo.id };
},
onSuccess: (data, variables, context) => {
// Replace temp with real data
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === context.tempId ? { ...data, _isPending: false } : todo
)
);
},
onError: (err, variables, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
});
User Feedback During Rollback
function TodoItem({ todo }) {
const deleteTodo = useMutation({/* ... */});
return (
<div className={`todo-item ${todo._isPending ? 'pending' : ''}`}>
<span>{todo.text}</span>
<button
onClick={() => deleteTodo.mutate(todo.id)}
disabled={deleteTodo.isPending}
>
{deleteTodo.isPending ? 'Deleting...' : 'Delete'}
</button>
</div>
);
}
Server Reconciliation
Server Reconciliation
Ensure UI consistency after optimistic updates.
Refetch Strategies
// After mutation, always refetch for consistency
const updateTodo = useMutation({
mutationFn: updateTodoApi,
onMutate: async (newTodo) => {
// Optimistic update
},
onError: (err, newTodo, context) => {
// Rollback
},
onSettled: () => {
// Always refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// Or refetch specific queries
const addProduct = useMutation({
mutationFn: createProduct,
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
queryClient.invalidateQueries({ queryKey: ['productStats'] });
},
});
Handling Server Response Differences
const addTodo = useMutation({
mutationFn: (text) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
}).then(r => r.json()),
onMutate: async (text) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
const tempTodo = {
id: 'temp-' + Date.now(),
text,
completed: false,
createdAt: new Date().toISOString(),
};
queryClient.setQueryData(['todos'], (old) => [...old, tempTodo]);
return { previousTodos, tempId: tempTodo.id };
},
onSuccess: (serverTodo, variables, context) => {
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) => {
if (todo.id === context.tempId) {
// Use server data as source of truth
return {
...serverTodo,
// Keep any client-only fields if needed
_justAdded: true,
};
}
return todo;
})
);
},
onError: (err, variables, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
toast.error('Failed to add todo');
},
});
Real-time Collaboration
// Handle concurrent updates
const updateTodo = useMutation({
mutationFn: (todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify({
...todo,
version: todo.version, // Optimistic concurrency control
}),
}).then(r => {
if (r.status === 409) {
throw new Error('Conflict detected');
}
return r.json();
}),
onError: (err) => {
if (err.message === 'Conflict detected') {
// Refetch to get latest state
queryClient.invalidateQueries({ queryKey: ['todos'] });
toast.warning('Item was updated by another user. Refreshing...');
}
},
});
Best Practices
- Always refetch after mutation for consistency
- Use temp IDs for new items
- Show pending state for in-flight items
- Handle conflicts with version control
- Provide user feedback during rollback
- Never lose user data - always rollback gracefully
Practice Problems
Create a reusable React component implementing Optimistic Updates. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Optimistic Updates using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Optimistic Updates for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. Why cancel queries before optimistic updates?
2. What should you return from onMutate for rollback?
3. When should you use optimistic updates?
4. What is server reconciliation?
5. How should you handle conflicts in optimistic updates?
Flashcards
Question
What is the purpose of onMutate in React Query?
Click to reveal answer
Answer
To perform optimistic updates, cancel queries, and save previous state for rollback.
Question
Why always refetch after optimistic updates?
Click to reveal answer
Answer
To ensure the UI eventually reflects the actual server state and resolve any discrepancies.
Question
What is a temp ID?
Click to reveal answer
Answer
A temporary identifier assigned to optimistic items before receiving the real ID from the server.
Question
What is optimistic concurrency control?
Click to reveal answer
Answer
Using version numbers to detect and handle conflicts when multiple users modify the same data.
Question
What is Optimistic Updates?
Click to reveal answer
Answer
Optimistic Updates is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Optimistic updates provide instant feedback for predictable operations
- 2.Always cancel queries and save previous state for rollback
- 3.Refetch after mutations to ensure server consistency
- 4.Use temp IDs for new items before server confirmation
- 5.Handle conflicts with version control and user feedback
Interview Tips
- •Explain the optimistic update pattern with React Query
- •Describe how to handle rollback on errors
- •Discuss when optimistic updates are appropriate vs when to avoid them
Cheat Sheet
Optimistic Updates Cheat Sheet
React Query Pattern
const mutation = useMutation({
mutationFn: apiCall,
onMutate: async (data) => {
await queryClient.cancelQueries({ queryKey });
const previous = queryClient.getQueryData(queryKey);
queryClient.setQueryData(queryKey, optimisticUpdate);
return { previous };
},
onError: (err, data, context) => {
queryClient.setQueryData(queryKey, context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey });
}
});
Key Steps
- Cancel outgoing queries
- Snapshot previous state
- Update cache optimistically
- Return context with snapshot
- Rollback on error
- Refetch on settlement