web/.agents/skills/tanstack-query-best-practices/rules/perf-select-transform.md
The select option transforms query data before it reaches your component. Use it for filtering, sorting, or deriving data. Benefits include memoization (re-runs only when data changes) and reduced component re-renders.
// Transforming in component - runs on every render
function CompletedTodos() {
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// This filtering runs on every render
const completedTodos = todos?.filter(todo => todo.completed) ?? []
const sortedTodos = [...completedTodos].sort((a, b) =>
new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()
)
return <TodoList todos={sortedTodos} />
}
// Using select - runs only when data changes
function CompletedTodos() {
const { data: completedTodos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) =>
todos
.filter(todo => todo.completed)
.sort((a, b) =>
new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()
),
})
return <TodoList todos={completedTodos ?? []} />
}
// Derive computed values
function TodoStats() {
const { data: stats } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
pending: todos.filter(t => !t.completed).length,
completionRate: todos.length
? (todos.filter(t => t.completed).length / todos.length) * 100
: 0,
}),
})
return (
<div>
<span>{stats?.completed} / {stats?.total} completed</span>
<span>({stats?.completionRate.toFixed(1)}%)</span>
</div>
)
}
// When select depends on external values, stabilize with useCallback
function FilteredTodos({ status }: { status: 'all' | 'active' | 'completed' }) {
const selectTodos = useCallback(
(todos: Todo[]) => {
switch (status) {
case 'active':
return todos.filter(t => !t.completed)
case 'completed':
return todos.filter(t => t.completed)
default:
return todos
}
},
[status]
)
const { data: filteredTodos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: selectTodos,
})
return <TodoList todos={filteredTodos ?? []} />
}
// Select single item from cached list
function useTodoById(id: number) {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => todos.find(todo => todo.id === id),
})
}
// Usage - shares cache with list query
function TodoDetail({ id }: { id: number }) {
const { data: todo } = useTodoById(id)
if (!todo) return <div>Todo not found</div>
return <div>{todo.title}</div>
}
| Scenario | Use Select? |
|---|---|
| Filtering list data | Yes |
| Sorting data | Yes |
| Computing derived values | Yes |
| Picking single item from list | Yes |
| Heavy transformations | Yes (memoized) |
| Simple data pass-through | No |
| Transformation needs external state | Yes, with useCallback |
select leverages structural sharing - only re-runs when data actually changesselect on the same queryuseCallback when needed