Опция select в TanStack Query предназначена для
трансформации данных сразу после получения результата запроса и до
передачи данных в компонент. Это один из важнейших механизмов
библиотеки, позволяющий:
Без select компоненты часто превращаются в место
постоянной обработки данных:
const users = data?.users
?.filter(user => user.active)
?.sort((a, b) => a.name.localeCompare(b.name))
Такой подход приводит к ряду проблем:
select переносит эту логику непосредственно в слой
запросов.
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => {
return data.users
}
})
Порядок работы:
queryFnselectselect становится dataПредположим, API возвращает:
{
"status": "success",
"meta": {
"count": 150
},
"users": [
{
"id": 1,
"name": "Alex"
},
{
"id": 2,
"name": "John"
}
]
}
Без select:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
console.log(data.users)
С select:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.users
})
console.log(data)
Теперь data содержит уже массив пользователей.
Очень распространённый сценарий — получение только нужной части ответа.
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
select: (response) => response.items
})
Компонент больше не знает структуру API.
Это особенно важно при:
API часто возвращают неудобные названия полей.
Например:
{
"user_id": 1,
"user_name": "Alex",
"user_email": "alex@test.com"
}
Через select можно нормализовать структуру:
const { data } = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
select: (user) => ({
id: user.user_id,
name: user.user_name,
email: user.user_email
})
})
Теперь все компоненты работают с единой структурой данных.
select идеально подходит для фильтрации.
const { data } = useQuery({
queryKey: ['tasks'],
queryFn: fetchTasks,
select: (tasks) => {
return tasks.filter(task => task.completed)
}
})
Компонент получает уже готовый список завершённых задач.
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
return [...users].sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
})
Важно создавать копию массива:
[...users]
Потому что sort() мутирует массив.
Мутация кэшированных данных TanStack Query — серьёзная ошибка.
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
select: (products) => {
return products
.filter(product => product.inStock)
.sort((a, b) => a.price - b.price)
}
})
select часто используется для создания derived data.
const { data } = useQuery({
queryKey: ['orders'],
queryFn: fetchOrders,
select: (orders) => {
return orders.map(order => ({
...order,
total: order.price * order.quantity
}))
}
})
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
select: (posts) => {
return posts.map(post => ({
...post,
formattedDate: new Date(post.createdAt)
.toLocaleDateString()
}))
}
})
const { data } = useQuery({
queryKey: ['cart'],
queryFn: fetchCart,
select: (items) => {
return {
items,
total: items.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
}
}
})
const { data } = useQuery({
queryKey: ['employees'],
queryFn: fetchEmployees,
select: (employees) => {
return employees.reduce((groups, employee) => {
const department = employee.department
if (!groups[department]) {
groups[department] = []
}
groups[department].push(employee)
return groups
}, {})
}
})
Частая задача — создание словаря по id.
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
return users.reduce((acc, user) => {
acc[user.id] = user
return acc
}, {})
}
})
Результат:
{
1: { id: 1, name: 'Alex' },
2: { id: 2, name: 'John' }
}
Это значительно ускоряет поиск:
data[userId]
вместо:
users.find(user => user.id === userId)
select вызывается не на каждый рендер компонента.
TanStack Query оптимизирует вызовы:
select не
будет;Это одно из важнейших преимуществ перед вычислениями прямо в компоненте.
Без select:
const filteredUsers = data?.filter(user => user.active)
Каждый рендер создаёт новый массив.
Это приводит к:
memo;С select:
select: (users) => users.filter(user => user.active)
вычисление выполняется внутри TanStack Query.
Важно понимать:
select не изменяет данные в кэше.
Кэш всегда хранит оригинальный результат queryFn.
Например:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.filter(user => user.active)
})
В кэше останется полный список пользователей.
Фильтрация применяется только для конкретного observer.
Это крайне важная особенность архитектуры.
Один и тот же запрос может использоваться с разными трансформациями.
const activeUsersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.filter(user => user.active)
})
const adminUsersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.filter(user => user.role === 'admin')
})
Сетевой запрос будет один.
Но данные для компонентов будут разными.
select: (data) => {
return veryHeavyCalculation(data)
}
Если вычисления тяжёлые:
то select может стать bottleneck.
Иногда лучше:
Ошибка внутри select переводит query в состояние
error.
Пример:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => {
return data.users.map(user => user.name)
}
})
Если data.users отсутствует:
Cannot read properties of undefined
query станет ошибочной.
select: (data) => {
return data?.users?.map(user => user.name) ?? []
}
select: (data) => {
return data?.items ?? []
}
TanStack Query отлично выводит типы после трансформации.
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
return users.map(user => user.name)
}
})
Тип data автоматически станет:
string[]
Без select:
User[]
С select:
number
Пример:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.length
})
select работает и с useInfiniteQuery.
Структура данных:
{
pages: [],
pageParams: []
}
Пример:
const query = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextCursor,
select: (data) => {
return {
...data,
pages: data.pages.flat()
}
}
})
Очень распространённая практика:
select: (data) => ({
...data,
pages: data.pages.flatMap(page => page.items)
})
Очень полезный подход — скрывать трансформацию внутри hooks.
function useActiveUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => {
return users.filter(user => user.active)
}
})
}
Компонент получает уже подготовленные данные.
Хороший custom hook:
Можно выносить преобразования в отдельные функции.
function selectActiveUsers(users) {
return users.filter(user => user.active)
}
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: selectActiveUsers
})
function filterActive(users) {
return users.filter(user => user.active)
}
function sortByName(users) {
return [...users].sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
function transformUsers(users) {
return sortByName(filterActive(users))
}
{
"meta": {},
"pagination": {},
"debug": {},
"items": []
}
Нельзя:
select: (data) => {
localStorage.setItem('users', JSON.stringify(data))
return data
}
select должен быть чистой функцией.
Нельзя:
select: async (data) => {
return await something(data)
}
select должен возвращать результат синхронно.
Правильный select:
Неправильно:
select: (users) => {
users.sort((a, b) => a.age - b.age)
return users
}
Потому что изменяется оригинальный массив.
Правильно:
select: (users) => {
return [...users].sort((a, b) => a.age - b.age)
}
Очень часто frontend не должен знать:
select превращается в слой адаптации.
const useProducts = () => {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
select: (response) => {
return response.data.map(product => ({
id: product.product_id,
title: product.product_name,
price: Number(product.product_price),
inStock: product.available === 1
}))
}
})
}
UI работает с удобной и предсказуемой структурой.
При грамотном использовании:
Именно поэтому select считается одним из ключевых
инструментов TanStack Query для построения чистой архитектуры клиентских
приложений.