In TanStack Query v5, what does structural sharing do to data after a background refetch, and when would you disable it?
answer
- keep references when nothing changed
- deep compare of old and new
- plain objects and arrays only
- memo dependencies stay stable
- off, or your own function
basics
~20 sStructural sharing deep-compares refetched data with the cached data and keeps old references for every unchanged part, so an identical response leaves data's reference unchanged. Disable it with structuralSharing: false for very large payloads or non-JSON data.
solid answer
~40 sAfter each fetch, TanStack Query v5 merges the new result with the previous data (option `structuralSharing`, default `true`). If the response is deeply equal, `data` keeps the same reference; if one item changed, unchanged siblings keep theirs and only the changed branch is new. That keeps `useMemo` and `useEffect` dependencies and memoized children stable across the frequent background refetches that `staleTime: 0` produces. It only understands plain objects and arrays; a newly created `Date`, `Map` or class instance always counts as changed. Turn it off with `structuralSharing: false` when payloads are so large that the comparison costs more than it saves, or pass a function `(oldData, newData) => …` to implement your own sharing.
code
ts · 15 linesimport { useQuery } from '@tanstack/react-query'
declare function fetchSnapshot(): Promise<{ version: number; rows: unknown[] }>
export function useSnapshot() {
return useQuery({
queryKey: ['snapshot'],
queryFn: fetchSnapshot,
structuralSharing: (oldData, newData) => {
const prev = oldData as { version: number } | undefined
const next = newData as { version: number }
return prev && prev.version === next.version ? prev : next
},
})
}go deeper
Recall that TanStack Query keeps the same data reference when a refetch returns identical content, and that the option is called structuralSharing.
Explain the deep compare that reuses unchanged branches, why it only works for plain objects and arrays, and how it keeps memo dependencies stable.
Recognise when a large or non-JSON payload makes sharing costly or useless, and choose between disabling it, a custom function, or keeping plain JSON in the cache.
Decide whether the cache should hold only serializable data as a team rule, trading conversion cost in render for stable references and simpler tooling.
## The problem structural sharing solves Every fetch parses the response into **brand-new objects**. If TanStack Query stored those as they arrived, `data` would be a new reference after every refetch, even when the server returned exactly the same content. With the default `staleTime` of `0`, refetches happen often (on mount, on window refocus, on reconnect), so every one of them would: - re-run `useMemo` and `useEffect` calls that list `data` or a piece of it as a dependency; - re-render children wrapped in `React.memo` that receive rows of `data` as props; - make any reference-equality check downstream report a change that did not happen. **Structural sharing** is the default answer. After each fetch, the new result is merged with the previous one so that unchanged parts keep their old references. ## What happens on each fetch 1. The query function resolves with the new data. 2. The library checks the **`structuralSharing`** option. If it is a function, that function receives the old and new data and its return value is stored. If it is `false`, the new data is stored as is. Otherwise, the default `true`, the library calls its internal deep-merge helper, `replaceEqualDeep`. 3. The helper compares old and new values. If they are the same reference, the old one is kept. If both are **plain arrays** or both are **plain objects**, it walks them key by key and reuses every child that is deeply equal. 4. If every child matched, the **old container is returned**, so `data` keeps its previous reference. If some child differs, a new container is built that holds the unchanged children's old references plus the new values. 5. Anything that is not a plain object or array, such as a `Date`, `Map`, `Set` or class instance, is taken from the new data, so a newly created one always counts as changed; only the very same instance is kept. ## What the result looks like | Refetch returns | `data` | unchanged items | changed or added items | |---|---|---|---| | identical content | same reference | same references | none | | one item's title changed | new array | same references | new object for that item | | one item appended | new array | same references | new object for the new item | Combined with **tracked properties**, where a `useQuery` result only re-renders the component for the properties it actually reads, a component that reads only `data` does not re-render at all after a refetch that returned identical content. ## When to disable or replace it The documentation is explicit that almost every app should leave it on: the comparison is cheap for typical payloads, and the stable references it produces save far more work downstream than they cost. The exceptions: - **Very large responses.** The comparison walks the whole tree on every fetch. For thousands of rows or deeply nested payloads that change constantly, the cost can exceed the benefit; set `structuralSharing: false` on that query. - **Non-JSON data.** If the query function returns freshly built class instances, `Map`s or `Date`s, sharing cannot help, because each new instance counts as changed. Either keep plain JSON in the cache and convert in render or in `select`, or supply a custom function. - **A cheaper change signal.** If the payload carries its own version or hash, a custom function can compare that single field and return the old data when it matches. ```ts import { useQuery } from '@tanstack/react-query' declare function fetchSnapshot(): Promise<{ version: number; rows: unknown[] }> export function useSnapshot() { return useQuery({ queryKey: ['snapshot'], queryFn: fetchSnapshot, structuralSharing: (oldData, newData) => { const prev = oldData as { version: number } | undefined const next = newData as { version: number } return prev && prev.version === next.version ? prev : next }, }) } ``` ## Common misreadings - Structural sharing is not **request deduplication**. Deduplication merges concurrent fetches for one key into a single request; sharing happens after a response has arrived. - It does not **skip the network**. The request always runs; only the stored references are reused. - It is not **immutability**. The references it keeps are the cache's own objects, so mutating `data` in place changes the cached entry for every reader of that key; treat query data as read-only and write through the query client instead. - It does not change **values**. With it on or off, the cache holds equal content; only object identity differs.
- A query function converts ISO strings into Date objects. What happens to structural sharing?Date instances are not plain objects, so the comparison treats each one as changed. Every object holding a `Date`, and every container above it, gets a new reference on every refetch. Keep the strings in the cache and convert in render or `select`, or supply a custom `structuralSharing` function.
- Does turning structuralSharing off change what is stored in the cache?The stored content is equal either way; only identity differs. With `structuralSharing: false` the cache holds exactly what the query function returned, a new reference after every fetch, so reference-based memoization downstream re-runs after each refetch.
saying these in an interview costs you the question
- Structural sharing means components on the same key share one network request.
- It preserves references for any value, including Map, Set and class instances.
- By default a refetch that returns identical JSON still gives data a new reference.
- Structural sharing lets TanStack Query skip the request when the data has not changed.
- Turning structuralSharing off is a routine performance fix for most apps.