В основе работы библиотеки TanStack Query лежит система идентификации запросов через query keys. Каждый запрос в кеше должен иметь уникальный ключ, по которому библиотека определяет:
Query key — это не просто строка. Это структурированный идентификатор состояния запроса.
Библиотека строит всю архитектуру кеширования именно вокруг ключей.
Наиболее простая форма:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Здесь:
['users']
— уникальный идентификатор запроса.
Хотя визуально это массив, внутри библиотеки ключ сериализуется в стабильное значение и используется как адрес кеша.
В ранних версиях React Query разрешались строковые ключи:
queryKey: 'users'
Современный подход в TanStack Query основан исключительно на массивах:
queryKey: ['users']
Причины:
['users']
['users', 1]
['users', 1, 'posts']
['products', category, sort]
queryClient.invalidateQueries({
queryKey: ['users']
})
Инвалидируются:
['users']
['users', 1]
['users', 2]
['users', 5, 'posts']
Обычно query key состоит из нескольких уровней.
Пример:
['posts', postId, 'comments']
Структура:
| Элемент | Назначение |
|---|---|
'posts' |
тип сущности |
postId |
идентификатор |
'comments' |
вложенный ресурс |
['users']
['users', userId]
['users', userId, 'posts']
['products', category, sort]
Query key напрямую влияет на жизненный цикл запроса.
Пример:
const result = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
Когда меняется:
userId
меняется и query key:
['user', 1]
['user', 2]
Для библиотеки это уже два разных запроса.
Следствия:
Ключ должен быть:
Обычно используются:
['users']
['users', 1]
['users', 'active']
['products', { page: 1 }]
Допускаются:
Очень распространённый подход:
['products', {
page: 1,
sort: 'price',
category: 'phones'
}]
Это особенно удобно при большом количестве параметров.
Библиотека использует глубокое стабильное сравнение.
Пример:
['products', { page: 1 }]
и
['products', { page: 1 }]
считаются одинаковыми, даже если объекты созданы заново.
Порядок элементов имеет значение.
Разные ключи:
['users', 1]
['users', 2]
Разные ключи:
['products', 'phones', 'price']
['products', 'price', 'phones']
Порядок полей объекта обычно не влияет:
['products', {
page: 1,
sort: 'price'
}]
и
['products', {
sort: 'price',
page: 1
}]
считаются одинаковыми.
Плохая практика:
['users', new Date()]
или:
['users', Math.random()]
или:
['users', () => {}]
Проблемы:
Ключ полностью определяет запись кеша.
Например:
['todos']
и
['todos', { completed: true }]
— две разные записи.
Каждая хранит:
Одно из главных преимуществ системы.
Пример:
['users']
['users', 1]
['users', 1, 'posts']
['users', 1, 'comments']
Такой подход позволяет:
Пример:
queryClient.invalidateQueries({
queryKey: ['users']
})
Будут затронуты:
['users']
['users', 1]
['users', 2]
['users', 5, 'posts']
Но не:
['posts']
Иногда требуется инвалидировать только конкретный ключ.
queryClient.invalidateQueries({
queryKey: ['users'],
exact: true
})
Теперь инвалидируется только:
['users']
['user-posts-comments']
Проблемы:
['users', userId, 'posts', 'comments']
Преимущества:
В крупных проектах ключи централизуют.
Без единого подхода появляются:
['user']
['users']
['userData']
['profile']
Это приводит к:
Пример:
export const userKeys = {
all: ['users'],
lists: () => [...userKeys.all, 'list'],
list: (filters) => [
...userKeys.lists(),
filters
],
details: () => [...userKeys.all, 'detail'],
detail: (id) => [
...userKeys.details(),
id
]
}
Использование:
useQuery({
queryKey: userKeys.detail(5),
queryFn: () => fetchUser(5)
})
Все ключи находятся в одном месте.
Структура становится стандартизированной.
Меньше вероятности опечаток:
['usrs']
queryClient.invalidateQueries({
queryKey: userKeys.all
})
Часто ключ строится как namespace.
Пример:
['dashboard']
['dashboard', 'stats']
['dashboard', 'charts']
Или:
['admin', 'users']
['admin', 'settings']
Это особенно полезно в больших приложениях.
Пагинация всегда должна быть частью ключа.
Неправильно:
['posts']
Правильно:
['posts', page]
Иначе страницы будут перезаписывать кеш друг друга.
Фильтры тоже должны входить в ключ.
Неправильно:
['products']
Правильно:
['products', filters]
Иначе библиотека не поймёт, что данные изменились.
Сортировка должна быть частью cache identity.
['products', {
sort: 'price'
}]
Infinite query также зависит от ключа.
useInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed
})
Если фильтр изменится:
['feed', category]
создастся новая цепочка страниц.
Prefetch использует те же ключи.
queryClient.prefetchQuery({
queryKey: ['users', 1],
queryFn: () => fetchUser(1)
})
Позже:
useQuery({
queryKey: ['users', 1],
queryFn: () => fetchUser(1)
})
использует уже готовый кеш.
При SSR ключи становятся особенно важными.
Сервер:
['posts', 1]
Клиент:
['posts', 1]
Ключи должны совпадать полностью.
Иначе hydration не сможет связать серверные данные с клиентским кешем.
Инвалидизация строится вокруг ключей.
Пример:
queryClient.invalidateQueries({
queryKey: ['posts']
})
После мутации:
await createPost(data)
можно обновить все связанные запросы.
queryClient.refetchQueries({
queryKey: ['notifications']
})
Библиотека найдёт соответствующие queries по ключам.
queryClient.removeQueries({
queryKey: ['drafts']
})
Записи кеша удаляются по ключу.
Ключ определяет, какие данные обновлять вручную.
queryClient.setQueryData(
['user', 5],
updater
)
Optimistic update работает только при правильной структуре ключей.
Пример:
queryClient.setQueryData(
['todos'],
old => [...old, optimisticTodo]
)
Плохо:
['data']
Проблемы:
Плохо:
[
'users',
userId,
Date.now()
]
Каждый рендер создаёт новый query.
Плохо:
['users', id]
['user', id]
['users-detail', id]
Следствие — хаос в кеше.
Обычно используется такой порядок:
[
resource,
scope,
params
]
Пример:
[
'products',
'list',
{
page: 1,
sort: 'price'
}
]
export const productKeys = {
all: ['products'],
lists: () => [
...productKeys.all,
'list'
],
list: (filters) => [
...productKeys.lists(),
filters
],
details: () => [
...productKeys.all,
'detail'
],
detail: (id) => [
...productKeys.details(),
id
]
}
В больших приложениях query keys становятся частью общей архитектуры.
Обычно определяются:
Часто используется readonly tuple:
const userKey = ['users', id] as const
Это помогает:
Query key в TanStack Query — это:
Чем лучше организована структура query keys, тем: