Back to Ragflow

mut-optimistic-updates: Implement Optimistic Updates for Responsive UI

web/.agents/skills/tanstack-query-best-practices/rules/mut-optimistic-updates.md

0.25.13.9 KB
Original Source

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

ApproachUse When
Cache ManipulationUpdate appears in multiple places, complex data structures
UI VariablesUpdate 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