In TanStack Query v5, what does queryClient.setQueryData do to a cache entry, and why must its updater never mutate the old data in place?
answer
- synchronous write, no request
- creates the entry if missing
- undefined means leave it alone
- the entry becomes fresh
- references drive re-renders
basics
~20 ssetQueryData synchronously writes data for one exact key, creating the entry if needed, marks it successful and fresh, and notifies observers. Returning undefined leaves the cache untouched. Mutating old data in place keeps the same reference, so dependent UI can miss the change.
solid answer
~50 s`setQueryData(queryKey, updater)` writes to exactly one key, synchronously, with no request. The updater is either a value or a function of the current data, which may be `undefined`. If the result is `undefined`, nothing happens and no entry is created; otherwise the entry is created if missing and gets `status: 'success'`, a new `dataUpdatedAt` and its invalidated flag cleared - so by `staleTime` it counts as freshly fetched - and every observer is notified. The update must be immutable. If the updater edits `oldData` and returns the same object, the cache holds the same reference: components that only read `data`, `select` memoization and React memo dependencies can all see "no change", while any snapshot you kept for a rollback was silently edited too. For several keys at once there is `setQueriesData(filters, updater)`, which only updates entries that already exist.
go deeper
Recall that setQueryData writes data for a key immediately without a request, and that the updater receives the current data.
Explain the effects on status, dataUpdatedAt and staleness, the undefined no-op, and the reference-based reasons the updater must be immutable.
Combine writes and invalidation after a mutation: write what the server returned, invalidate what it derives, and use setQueriesData for entities repeated across lists.
Decide where manual cache writes are allowed in a large codebase, since each one duplicates server logic on the client and can drift from it.
## What the call does `queryClient.setQueryData(queryKey, updater)` is TanStack Query v5's way to write into the cache yourself: - **one key, exact** - it targets the single entry whose key hashes to `queryKey`; there is no prefix matching; - **synchronous** - the cache is updated before the call returns, and the new data is returned; - **no network** - nothing is fetched; you are asserting what the server state now is. The updater is either the new value or a function `(oldData) => newData`, where `oldData` may be `undefined` if nothing is cached yet. ## Effects on the entry When the updater produces a defined value, the entry: 1. is **created** if it did not exist; 2. gets **`status: 'success'`** and the new `data`; 3. gets a new **`dataUpdatedAt`** (now, or the `updatedAt` you pass in the third argument); 4. has its **invalidated flag cleared**; 5. **notifies** every observer, so mounted components re-render with the new data. Points 3 and 4 mean that, as far as `staleTime` is concerned, the data now looks **freshly fetched**. With a `staleTime` of five minutes, a manual write suppresses automatic refetches for five minutes - convenient when the write is exactly what the server returned, risky if it was a guess. When the updater returns **`undefined`**, the call is a no-op: no entry is created and nothing is notified. That makes this pattern safe: ```ts queryClient.setQueryData<Todo[]>(['todos', 'list'], (old) => old ? [...old, createdTodo] : old, ) ``` If the list was never loaded, nothing is written - which is right, because appending to an empty guess would create a fake one-item list. ## Why the updater must be immutable The v5 docs are blunt: do not mutate `oldData`, or data from `getQueryData`, in place. The failure modes follow from how the library detects change: | Mutating in place | Consequence | |---|---| | updater edits `old` and returns it | the new data **is** the old reference | | observers compare result properties | a component reading only `data` can decide nothing changed and skip its render | | `select` memoizes on the data reference | selected values are not recomputed | | rollback snapshots hold the same object | the "previous" value you saved was edited too | The correct form always builds new objects along the changed path: ```ts queryClient.setQueryData<Todo>(['todos', 'detail', id], (old) => old ? { ...old, title } : old, ) ``` ## Writing the mutation response into the cache The main use of `setQueryData` in this leaf is coordinating a successful write with the cache: when the server returns the updated entity, write it rather than refetch it. ```tsx import { useMutation, useQueryClient } from '@tanstack/react-query' function useRenameTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: renameTodo, onSuccess: (updated) => { queryClient.setQueryData(['todos', 'detail', updated.id], updated) return queryClient.invalidateQueries({ queryKey: ['todos', 'list'] }) }, }) } ``` This mix is common: the **detail** entry is written directly because the response is exactly its new state; the **lists** are invalidated because their order, filters and counts are the server's business. ## Several keys: `setQueriesData` `queryClient.setQueriesData(filters, updater)` applies one updater to every **existing** query that matches the filters (prefix matching applies here), and never creates entries. It is the tool for "rename this todo in every cached list": ```ts queryClient.setQueriesData<Todo[]>({ queryKey: ['todos', 'list'] }, (old) => old?.map((t) => (t.id === updated.id ? updated : t)), ) ``` ## Reading before writing: `getQueryData` The companion method `queryClient.getQueryData(queryKey)` returns the cached data for a key, or `undefined`. It is **imperative and non-reactive**: calling it during render does not subscribe the component, so the component will not update when the entry changes. Use it inside callbacks - for example to read the current list before computing an update - and use `useQuery` to read data for rendering. Like the updater's `old`, the value it returns must be treated as read-only. ## Checklist - Write immutably - spread, `map`, `filter`, never assign into `old`. - Return `old` (or `undefined`) when there is nothing to update. - Remember the entry now counts as fresh. - Write only what the server confirmed; invalidate what it computes.
- After setQueryData, will a query with staleTime: 60000 refetch when another component mounts it within the minute?No. The write sets a new `dataUpdatedAt` and clears the invalidated flag, so the entry counts as fresh for its `staleTime`, and the default `refetchOnMount: true` refetches only stale data. Invalidate afterwards if the server should confirm the write.
- What is the difference between setQueryData and setQueriesData when no entry exists yet?`setQueryData` creates the entry (unless the updater returns `undefined`). `setQueriesData` only updates queries that already exist and match the filters, so it never creates an entry.
saying these in an interview costs you the question
- Believes setQueryData triggers a refetch of the key it writes.
- Pushes into oldData and returns it, expecting the UI to update reliably.
- Thinks setQueryData matches by key prefix like invalidateQueries.
- Assumes returning undefined from the updater clears the cached data.
- Expects data written with setQueryData to still count as stale.