initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# mut-optimistic-updates: Implement Optimistic Updates for Responsive UI
|
||||
|
||||
## Priority: HIGH
|
||||
|
||||
## Explanation
|
||||
|
||||
Optimistic updates immediately reflect changes in the UI before the server confirms them, creating a snappy user experience. Implement them for user-initiated mutations where the expected outcome is predictable.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```tsx
|
||||
// No optimistic update - UI waits for server response
|
||||
const mutation = useMutation({
|
||||
mutationFn: toggleTodoComplete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
},
|
||||
})
|
||||
|
||||
// User clicks checkbox, waits 200-500ms for visual feedback
|
||||
```
|
||||
|
||||
## Good Example: Via Cache Manipulation
|
||||
|
||||
```tsx
|
||||
const mutation = useMutation({
|
||||
mutationFn: toggleTodoComplete,
|
||||
onMutate: async (todoId) => {
|
||||
// 1. Cancel outgoing refetches to prevent overwriting optimistic update
|
||||
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
||||
|
||||
// 2. Snapshot previous value for potential rollback
|
||||
const previousTodos = queryClient.getQueryData(['todos'])
|
||||
|
||||
// 3. Optimistically update the cache
|
||||
queryClient.setQueryData(['todos'], (old: Todo[]) =>
|
||||
old.map((todo) =>
|
||||
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
|
||||
)
|
||||
)
|
||||
|
||||
// 4. Return context for rollback
|
||||
return { previousTodos }
|
||||
},
|
||||
onError: (err, todoId, context) => {
|
||||
// Rollback on error
|
||||
queryClient.setQueryData(['todos'], context?.previousTodos)
|
||||
},
|
||||
onSettled: () => {
|
||||
// Refetch to ensure consistency regardless of success/failure
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: Via UI Variables (Simpler)
|
||||
|
||||
```tsx
|
||||
// When mutation only affects local UI, use mutation state directly
|
||||
function TodoItem({ todo }: { todo: Todo }) {
|
||||
const mutation = useMutation({
|
||||
mutationFn: toggleTodoComplete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
},
|
||||
})
|
||||
|
||||
// Show optimistic state while pending
|
||||
const displayCompleted = mutation.isPending
|
||||
? !todo.completed // Optimistic: show toggled state
|
||||
: todo.completed // Settled: show actual state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={displayCompleted}
|
||||
disabled={mutation.isPending}
|
||||
onChange={() => mutation.mutate(todo.id)}
|
||||
/>
|
||||
<span style={{ opacity: mutation.isPending ? 0.5 : 1 }}>
|
||||
{todo.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: Optimistic Create with Temporary ID
|
||||
|
||||
```tsx
|
||||
const createTodo = useMutation({
|
||||
mutationFn: (newTodo: CreateTodoInput) => api.createTodo(newTodo),
|
||||
onMutate: async (newTodo) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
||||
const previousTodos = queryClient.getQueryData(['todos'])
|
||||
|
||||
// Add with temporary ID
|
||||
const optimisticTodo = {
|
||||
id: `temp-${Date.now()}`,
|
||||
...newTodo,
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
queryClient.setQueryData(['todos'], (old: Todo[]) => [...old, optimisticTodo])
|
||||
|
||||
return { previousTodos, optimisticTodo }
|
||||
},
|
||||
onError: (err, newTodo, context) => {
|
||||
queryClient.setQueryData(['todos'], context?.previousTodos)
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
// Replace temp todo with real one
|
||||
queryClient.setQueryData(['todos'], (old: Todo[]) =>
|
||||
old.map((todo) =>
|
||||
todo.id === context?.optimisticTodo.id ? data : todo
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## When to Use Each Approach
|
||||
|
||||
| Approach | Use When |
|
||||
|----------|----------|
|
||||
| Cache Manipulation | Update appears in multiple places, complex data structures |
|
||||
| UI Variables | Update only visible in one component, simpler implementation |
|
||||
|
||||
## Context
|
||||
|
||||
- Always provide rollback logic in `onError`
|
||||
- Cancel queries before optimistic update to prevent race conditions
|
||||
- Call `invalidateQueries` in `onSettled` to sync with server truth
|
||||
- For forms, consider if validation should block optimistic display
|
||||
- Test error scenarios to verify rollback works correctly
|
||||
Reference in New Issue
Block a user