SuperAPI AI Directory

tanstack-query-intent-core-implement-optimistic-updates-20e8c58a

Data & analytics

View repository

Updated 10-16 00:02

About this skill

Setup

import { useMutation, useQueryClient } from '@tanstack/react-query'

type Todo = { id: number; title: string }

export function useOptimisticAddTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (title: string) => ({ id: Date.now(), title }), onMutate: async (title) => { await queryClient.cancelQueries({ queryKey: ['todos'] }) const previous = queryClient.getQueryData<Array>(['todos']) queryClient.setQueryData<Array>(['todos'], (old = []) => [ ...old, { id: -1, title }, ]) return { previous } }, onError: (_error, _title, context) => { queryClient.setQueryData(['todos'], context?.previous) }, onSettled: async () => { await queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) }


## Core Patterns

### Write mutation response into detail cache

```ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

export function useUpdateTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (todo: { id: number; title: string }) => todo,
    onSuccess: (todo) => queryClient.setQueryData(['todo', todo.id], todo),
  })
}

Update lists immutably

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()

queryClient.setQueryData<Array<{ id: number; title: string }>>(
  ['todos'],
  (old = []) =>
    old.map((todo) => (todo.id === 1 ? { ...todo, title: 'Done' } : todo)),
)

Roll back with context

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }])
queryClient.setQueryData(['todos'], previous)

Common Mistakes

CRITICAL Optimistic write without cancellation

Wrong:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }])

Correct:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
await queryClient.cancelQueries({ queryKey: ['todos'] })
queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }])

An in-flight refetch can overwrite an optimistic write unless it is cancelled first.

Source: TanStack/query:docs/framework/react/guides/optimistic-updates.md

CRITICAL In-place mutation

Wrong:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
queryClient.setQueryData<Array<{ id: number; done: boolean }>>(
  ['todos'],
  (old = []) => {
    old[0].done = true
    return old
  },
)

Correct:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()
queryClient.setQueryData<Array<{ id: number; done: boolean }>>(
  ['todos'],
  (old = []) =>
    old.map((todo) => (todo.id === 1 ? { ...todo, done: true } : todo)),
)

Cache updates must be immutable so observers can detect and share changes correctly.

Source: TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md

HIGH Persisted persister loses optimistic mutation

Wrong:

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient()
queryClient.setMutationDefaults(['updateTodo'], {})

Correct:

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient()
queryClient.setMutationDefaults(['updateTodo'], {
  mutationFn: async (todo: { id: number; title: string }) => todo,
})

Paused persisted mutations need a default mutationFn after hydration because functions cannot be serialized.

Source: TanStack/query:docs/framework/react/plugins/persistQueryClient.md

See also: core/cancel-queries-and-consume-abort-signals before optimistic writes.