Render Optimizations
Purpose and Scope
Render optimization in TanStack Query is about avoiding component work that does not correspond to a meaningful change in the data a component actually reads. The React guide frames this as behavior that React Query applies automatically, rather than as a manual performance layer that every application must build. The main tools are structural sharing, tracked result properties, and selector-based subscriptions. Together, they let a query update its internal state frequently while keeping many React components from re-rendering for changes they never observe. This page focuses on React usage, but the observer behavior is grounded in the shared query core. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md, packages/query-core/src/tests/queryObserver.test.tsx
TanStack Query separates the identity of the top-level hook result from the stability of the data inside that result. The render optimization guide explicitly warns that the object returned from useQuery, useInfiniteQuery, useMutation, and the array returned from useQueries are not referentially stable. A component should therefore not treat the full result object as a memoized value. Instead, React Query works to keep data references as stable as possible and to notify consumers only when properties they used have changed. That distinction is central when debugging renders or designing custom hooks. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
Relevant Source Files
- docs/framework/react/guides/render-optimizations.md — Defines the public concepts for structural sharing, referential identity, tracked properties, select, and selector memoization.
- docs/framework/react/reference/useQuery.md — Lists the useQuery options and result fields that participate in render behavior, including notifyOnChangeProps, select, structuralSharing, subscribed, status flags, and fetch status fields.
- packages/react-query/src/tests/useQuery.test.tsx — Provides React adapter test coverage for the hook surface documented by the guide and reference page.
- packages/query-core/src/tests/queryObserver.test.tsx — Exercises QueryObserver subscription behavior, current results, disabled queries, pending and success transitions, and core observer notifications.
Core Concepts
Structural sharing is the first optimization to understand because it controls whether data values retain references between fetches. Network responses are commonly parsed into entirely new objects, even when their contents match the previous response. React Query compares JSON-compatible data and preserves unchanged references where possible. If nothing changed, the original data reference can remain in place; if only part of a structure changed, unchanged subtrees can keep their identity. This makes downstream memoization and child component props more effective without requiring every query function to implement reference preservation itself. Sources: docs/framework/react/guides/render-optimizations.md
There are important boundaries around structural sharing. The guide states that it works for JSON-compatible data, which is the normal shape for server responses but not every possible value returned by a query function. Applications can disable it by setting structuralSharing to false globally or for a single query, and they can provide a custom function when a domain-specific comparison is more appropriate. Use that escape hatch when data includes non-JSON values, when comparison cost is not worth the benefit, or when an application has its own immutable data strategy that should define reference reuse. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
Tracked properties are the second optimization, and they address a different problem: the hook result contains many fields that can change independently. A component that renders only data should not re-render simply because isFetching, isStale, or another status flag changed elsewhere in the result. React Query tracks property access through a Proxy get trap. Accessing a property, whether directly or by destructuring, marks it as used by that observer. Later notifications can then be scoped to the properties that the component actually consumed, reducing renders caused by unused result metadata. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
The tracked-property model has one practical pitfall: object rest destructuring reads broadly enough to disable the optimization. If a component takes the remaining fields from a query result, it effectively observes more than it needs, which can turn small state changes into unnecessary renders. The guide points readers to an ESLint rule that guards against this pattern. In application code, prefer explicit property access or explicit destructuring of only the fields the component needs, such as data and error, instead of collecting the entire remaining result object for convenience. Sources: docs/framework/react/guides/render-optimizations.md
System-to-Code Mapping
At the API level, render behavior is configured through documented useQuery options and expressed through documented result fields. The reference page lists notifyOnChangeProps for controlling property-based notifications, select for subscribing to transformed data, structuralSharing for reference reuse, and subscribed for observer participation. It also lists result fields such as data, error, status, fetchStatus, isFetching, isStale, isSuccess, and refetch. Those fields are exactly why tracked properties matter: a query result is rich, but many components only need a small subset of it. Sources: docs/framework/react/reference/useQuery.md
The shared QueryObserver is the core abstraction behind a framework hook observing cache state. The core tests show an observer triggering a fetch when subscribed, moving through pending and success states even when a query function returns synchronously, and reading cached data after subscription. They also show disabled queries staying idle and not fetching when invalidated until an explicit refetch path is used. These behaviors explain why React-level render optimization cannot simply be a React memo wrapper; it depends on the observer’s current result, subscription lifecycle, and notification decisions. Sources: packages/query-core/src/tests/queryObserver.test.tsx, packages/react-query/src/tests/useQuery.test.tsx
Selectors and Derived Subscriptions
The select option is the main way to narrow a component’s subscription to derived data. Instead of subscribing a component to the full todos array, a custom hook can accept a selector and return useQuery with that selector. A count component can select the array length, so it only needs to update when the number of todos changes. If a todo name changes but the length remains the same, the count consumer does not need to re-render for that data transformation. This is especially useful when large server responses feed small UI widgets. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
Selectors are not an error boundary or validation mechanism. The guide states that select operates on successfully cached data and is not the right place to throw errors. If a select function returns an error value, the query can still be considered successful while data becomes undefined, which is usually not the intended failure model. Put fetch and validation failures in the query function when the query should fail, or handle non-cache-related UI error cases outside the query hook. This keeps query status aligned with the actual source of truth. Sources: docs/framework/react/guides/render-optimizations.md
Selector memoization is also part of the rendering story. A select function re-runs when the function reference changes or when the underlying data changes. An inline selector therefore changes on every render unless it is wrapped in useCallback or moved to a stable function outside the component. For simple selectors without component dependencies, extraction is often the clearest option. For selectors that close over props or local state, useCallback communicates when the selector should be recreated and prevents repeated transformation work caused only by a new function identity. Sources: docs/framework/react/guides/render-optimizations.md
Practical Usage Guidance
When building a custom query hook, design the returned shape around what callers actually need. Returning the entire query result is flexible, but it encourages consumers to observe many fields and can hide render causes. A focused hook can expose data, a small number of status flags, and an optional selector parameter for derived values. If callers need background fetch indicators, expose isFetching intentionally. If they only render cached data, avoid reading status fields in that component. This practice aligns component subscriptions with UI responsibility and makes render changes easier to reason about. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
Use notifyOnChangeProps only when the default tracked-property behavior is not enough. The guide describes it as customizable globally or per query, and notes that setting it to all turns off the tracked-property optimization. That can be useful for debugging, compatibility, or a wrapper that deliberately wants every result change, but it should not be the default performance strategy. In normal React components, explicit property reads plus selectors usually produce the intended behavior with less configuration and fewer surprises than manually maintaining a notification-property list. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md
Testing Signals
The core observer tests are useful when verifying a mental model for render behavior because they show how query state changes before React sees it. A synchronous query function still produces a pending result with a fetching fetch status before resolving to success and idle. A subscribed observer can read existing cache data immediately. A disabled query can remain pending and idle without fetching, even when invalidated. These details explain why result flags may change independently from data and why tracked properties are valuable: a component should only react to the fields it uses. Sources: packages/query-core/src/tests/queryObserver.test.tsx
For React users, the reference and guide should be read together with adapter tests. The reference names the public options and fields, the guide explains the performance contract, and the tests protect the hook behavior that bridges React to QueryObserver. When diagnosing an unexpected render, first check whether the component accessed a changing property, then check whether a selector reference is stable, and then check whether structural sharing can preserve the data reference for the returned value. This order follows the documented layers from hook result to selected data to observer notification. Sources: docs/framework/react/guides/render-optimizations.md, docs/framework/react/reference/useQuery.md, packages/react-query/src/tests/useQuery.test.tsx, packages/query-core/src/tests/queryObserver.test.tsx
Next Steps
After applying these patterns, review the related pages that influence when query results change in the first place. Query Keys determine cache identity, Important Defaults explain staleness and background refetching, and Render Optimizations should be paired with Queries for status and fetch-status semantics. If a component still renders more often than expected, inspect the exact properties it reads, remove rest destructuring, stabilize selectors, and decide whether structuralSharing should stay enabled, be disabled, or be replaced with a custom function for the data shape.