# Tanstack Query Intent Core Implement Optimistic Updates 20e8c58a

> Use this when implementing optimistic updates, onMutate rollback context, cancelQueries before writes, setQueryData, setQueriesData, immutable cache writes, mutation rollback, and offline-aware optimistic state.

- Skill: `lukasa1993/tanstack-query-intent-core-implement-optimistic-updates-20e8` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lukasa1993/tanstack-query-intent-core-implement-optimistic-updates-20e8`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lukasa1993/tanstack-query-intent-core-implement-optimistic-updates-20e8/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lukasa1993 (https://skillmd.com/u/lukasa1993)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lukasa1993/tanstack-query-intent-core-implement-optimistic-updates-20e8

---


## Setup

```ts
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<Todo>>(['todos'])
      queryClient.setQueryData<Array<Todo>>(['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

```ts
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

```ts
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:

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

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

Correct:

```ts
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:

```ts
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:

```ts
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:

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

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

Correct:

```ts
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.

