Skip to main content

structural sharing

Description: When a refetch (or any cache write) produces a payload that is deeply equal to what is already cached, the cache keeps the previous object reference instead of storing the new one. Subscribers that rely on referential equality therefore don't re-render on structurally identical data.

Default Value: true (enabled). Configured on the DataClient cache via the structuralSharing option; set it to false to opt out.

info

This behavior is applied automatically inside the DataClient cache — there is no useFetch / useSuspenseFetch option for it. It affects the identity of the data you receive from both hooks.

Why it matters

useFetch and useSuspenseFetch read the cache through useSyncExternalStore, and consumers frequently pass data into React.memo, useMemo dependency arrays, or useEffect dependencies. Without structural sharing, every refetch, poll, or focus-triggered request would create a new object — even when the server returned the exact same bytes — forcing those consumers to re-render or re-run effects needlessly.

With structural sharing, only the subtrees that actually changed get new references; everything that stayed the same keeps its old reference. If nothing changed, data is referentially identical across the refetch.

function ProductPrice({ id }: { id: string }) {
// Polls every 30s. If the price object is unchanged, `data` keeps the same
// reference, so this memoized child does NOT re-render on each poll.
const { data } = useFetch<Product>(['product', id], () => fetchProduct(id), {
poll: 30_000
});

return <MemoizedPriceTag price={data?.price} />;
}

How it works step by step

  1. A request resolves → the cache is about to write a new response.
  2. Structural sharing compares the incoming response with the previously cached one, recursively (replaceEqualDeep).
  3. Deeply-equal subtrees keep their previous reference; only changed subtrees are replaced with the new value.
  4. If the whole payload is deeply equal → the previous response reference is reused verbatim.
  5. useSyncExternalStore sees an identical snapshot → no re-render is scheduled for that subscriber.

The comparison handles plain objects and plain arrays (including objects from another realm, e.g. an iframe). Non-plain values (class instances, Date, Map, etc.) are replaced as-is rather than merged.

Opting out

Structural sharing is on by default. Disable it at the cache level only if you specifically need every write to produce a fresh reference (for example, to force a re-render on identical data):

// DataClient cache configuration
{
structuralSharing: false;
}

See also