Capítulo 39 de 80

Chapter 39: Render Optimizations

Core Idea

Three automatic mechanisms keep re-renders minimal — structural sharing (stable references for unchanged data), tracked properties (re-render only on properties actually read), and select (subscribe to a derived subset) — but object rest destructuring silently defeats tracked-properties, and an inline select function silently defeats its own memoization.

Key Concepts

  • Structural sharing: on a successful refetch, unchanged parts of the data keep their old object reference; only genuinely changed parts get new references — works only for JSON-compatible data, and is configurable via structuralSharing: false or a custom diff function.
  • Referential identity caveat: the top-level object from useQuery/useInfiniteQuery/useMutation (and the array from useQueries) is a new reference every render — only data itself benefits from structural-sharing stability.
  • Tracked properties: a Proxy-based mechanism triggers re-renders only for properties actually accessed (destructured or read directly) — isFetching/isStale changing doesn't re-render a component that never reads them. Configurable via notifyOnChangeProps (set to 'all' to disable). Object rest destructuring silently disables this — the ESLint rule no-rest-destructuring exists specifically to catch this.
  • select: subscribes the component to a derived value instead of the raw data, re-rendering only when that derived value changes — e.g. useTodos((data) => data.length) re-renders only on count change, not on unrelated field edits.
  • select error handling: select operates only on already-successful cached data — it is not a place to throw; a select that throws internally results in data: undefined with isSuccess: true, which is a confusing state. Validate/error in queryFn (to genuinely fail the query) or outside the hook, not inside select.
  • select memoization: re-runs only if the select function's own reference changes or data changes — an inline arrow function re-creates on every render, so it re-runs every render too. Fix with useCallback or a module-level stable function reference.

Code Examples

// Inline select — re-runs every render (new reference each time)
export const useTodoCount = () => useTodos((data) => data.length)

// Stable reference — only re-runs when `data` actually changes
const selectTodoCount = (data) => data.length
export const useTodoCount = () => useTodos(selectTodoCount)
  • What it demonstrates: the difference between an inline select (loses memoization) and a hoisted stable reference (keeps it).

Key Takeaways

  1. Never object-rest-destructure a useQuery result if you want tracked-properties re-render skipping to keep working — enable the no-rest-destructuring ESLint rule to catch this automatically.
  2. Extract non-trivial select functions to a stable reference (module scope or useCallback) — an inline one defeats its own memoization every render.
  3. Keep error/validation logic out of select — it can only degrade to data: undefined silently, not fail the query the way a queryFn throw does.

Connects To

  • Important Defaults: structural sharing's role as one of the library's default behaviors.
  • ESLint Plugin Query: the no-rest-destructuring rule enforcing the tracked-properties caveat.