In TanStack Query v5, what does the select option of useQuery do, and why should the function you pass to it keep a stable reference?
answer
- shapes what the component receives
- the cache keeps the raw result
- reruns on data or function change
- inline arrows are new every render
basics
~20 sselect transforms or picks from the cached data for one observer; the cache keeps the raw queryFn result. It reruns only when the data or the select function's reference changes, so an inline function runs every render unless it is hoisted or memoized.
solid answer
~40 s`select` receives the data the `queryFn` stored and returns what this `useQuery` call hands to its component - for example `todos => todos.length` or `todos => todos.filter(t => !t.done)`. It does **not** change the cache: other components using the same key still get the full result, and there is still one request. The observer memoizes it: `select` runs again only when the cached data changes or when the `select` function itself is a different reference. An inline arrow is a new function on every render, so it reruns every render; hoist it to module scope or wrap it in `useCallback` when the transform is expensive. A second benefit is re-render narrowing: with the default tracked properties, a component that reads only the selected `data` re-renders only when that selected value changes.
go deeper
Recall that select reshapes what useQuery returns for one component while the cache keeps the original data.
Explain the two memoization triggers, why an inline arrow reruns every render, and how select narrows re-renders to changes in the selected value.
Decide where a transform belongs, queryFn or select, and design a shared hook that accepts a stable selector so call sites stay cheap.
Set a convention for query hooks across teams, such as one query definition per resource with selectors for views, to keep cache entries and requests from multiplying.
## What `select` is for `useQuery` in TanStack Query v5 accepts a **`select`** option: a function that takes the data stored in the cache for that query key and returns the value this particular hook call should expose as `data`. ```tsx import { useQuery } from '@tanstack/react-query' const selectOpenCount = (todos: Todo[]) => todos.filter((t) => !t.done).length function OpenBadge() { const { data: openCount } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: selectOpenCount, }) return <span>{openCount ?? 0}</span> } ``` Three properties define it: - **Per observer.** `select` shapes what *this* hook returns. The cache entry for `['todos']` still holds the full array returned by `fetchTodos`, and another component reading `['todos']` without `select` gets that full array. - **One request.** Several components can derive different views - a count, a filtered list, a single item - from one cached response, with one network request and one cache entry. - **Typed separately.** The query's cached type (`Todo[]`) and the returned `data` type (`number`) differ, and TypeScript infers both. ## When `select` runs: the memoization rule The query observer caches the last selected result. From the v5 source and docs, `select` runs again only if: 1. the underlying cached **data changed**, or 2. the **`select` function reference changed**. An inline arrow - `select: (todos) => todos.length` written inside the component - is a new function on every render, so rule 2 fires every render and the transform reruns every time. For a `length` that is harmless; for sorting or grouping thousands of rows it is wasted work. | How `select` is written | Reruns when | |---|---| | inline arrow in the component | every render | | function hoisted to module scope | only when the data changes | | `useCallback` with dependencies | when the data or a dependency changes | The `useCallback` form is the right one when the transform depends on props or state, such as a filter value. ## Fewer re-renders, not just less work By default `useQuery` tracks which result properties a component reads and re-renders it only when one of those changes. The selected value is compared against the previous one, and unchanged parts keep their references. So in the example above, a refetch that renames one todo leaves the open count at the same number: `data` is unchanged, and a component that reads only `data` does not re-render. Without `select`, the same component would receive a new array and re-render. ## What `select` is not - **Not a place to write the cache.** It cannot change what other consumers see; if every consumer needs the transformed shape, transform inside the `queryFn` instead so the cache stores it once. - **Not a validation step.** The docs recommend handling bad data in the `queryFn`. If `select` throws, that observer's result reports an error while the cache entry itself stays successful - only that hook is affected, which is rarely what you want for real data problems. - **Not free when inline and heavy.** Memoize it, as above. ## Transform in `queryFn` or in `select`? | Transform in `queryFn` | Transform in `select` | |---|---| | stored once, every consumer sees it | computed per hook, cache keeps raw data | | runs once per fetch | runs per observer when data or function changes | | right for normalising an API response | right for per-component views: counts, filters, single items | A common structure is a custom hook that accepts an optional selector, so each call site opts into its own slice while sharing one query definition. ```tsx import { useQuery } from '@tanstack/react-query' export function useTodos<T = Todo[]>(select?: (todos: Todo[]) => T) { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select }) } ``` The caller is then responsible for passing a stable selector, which is the same memoization rule applied one level up. ## Edge cases interviewers probe - **Placeholder data goes through `select` too.** If the query shows `placeholderData` while its first fetch runs, the component receives the *selected* placeholder, so the selector must handle that shape as well. - **`select` does not run without data.** While the query is `pending` with no data and no placeholder, `data` is `undefined` and the selector is not called; the component still needs its own `undefined` branch. - **Changing the selector does not refetch.** Swapping a filter inside a `useCallback` recomputes `data` from the cache immediately, with no network request - which is often exactly why a filter belongs in `select` and not in the key.
- Two components read ['todos'], one with select: todos => todos.length. How many requests and cache entries are there?One of each. `select` runs after the cache, per observer, so both components share the single `['todos']` entry and its request; only the value handed to each component differs.
- When would you move a transform out of select and into the queryFn?When every consumer needs the transformed shape, such as normalising field names from the API. Then the transform runs once per fetch and is stored in the cache, instead of being repeated in each `select`.
saying these in an interview costs you the question
- Believes select rewrites what is stored in the cache for that key.
- Thinks an inline select function runs only when the data changes.
- Expects each select to trigger its own network request.
- Throws from select expecting the whole query and its cache entry to fail.