В TanStack Query идентификация данных строится вокруг query key — структурированного ключа, который определяет уникальность запроса в кеше. Механизм мемоизации query keys напрямую влияет на производительность, стабильность кеша и предсказуемость повторного использования данных. Ошибки в проектировании ключей приводят к дублированию запросов, потере кеша или некорректной инвалидации.
Query key в TanStack Query представляет собой сериализуемую структуру, чаще всего массив, где каждый элемент влияет на уникальность запроса:
useQuery({
queryKey: ['users', userId],
queryFn: fetchUser
})
Мемоизация здесь означает, что один и тот же логически идентичный query key должен ссылаться на один и тот же кешированный результат, независимо от того, как именно был создан объект или массив в памяти.
Ключевой момент: TanStack Query не сравнивает query key по ссылке, он сравнивает его по структурному содержимому.
['users', 1] !== ['users', 1] // но считаются одинаковыми в кеше
Это достигается через нормализацию и сериализацию ключей перед использованием внутри внутреннего Map-структурированного кеша.
Внутри TanStack Query query key преобразуется в стабильное строковое или хеш-подобное представление. Это позволяет использовать его как индекс в кеше.
Упрощённо процесс выглядит так:
Важно, что порядок элементов массива имеет значение:
['users', 'active'] !== ['active', 'users']
Хотя оба массива содержат одинаковые значения, они формируют разные ключи.
Хотя TanStack Query не требует стабильности ссылок для корректной работы кеша, нестабильные query key могут приводить к лишним перерасчётам внутри React-слоя.
Проблемный пример:
useQuery({
queryKey: [{ type: 'users', page: 1 }],
queryFn: fetchUsers
})
Если объект создаётся заново при каждом рендере:
const key = { type: 'users', page: 1 }
useQuery({
queryKey: ['users', key],
queryFn: fetchUsers
})
Даже несмотря на структурную эквивалентность, React может пересоздавать зависимости, что приводит к лишним вычислениям на уровне сравнения зависимостей, хотя TanStack Query всё ещё корректно сопоставит ключи.
Query key может быть:
useQuery({
queryKey: 'users',
queryFn: fetchUsers
})
Такой подход допустим, но ограничивает масштабируемость, так как невозможно расширять параметры запроса без изменения ключевой структуры.
useQuery({
queryKey: ['users', { page: 1, limit: 20 }],
queryFn: fetchUsers
})
Составные ключи позволяют кодировать параметры запроса прямо в идентификатор кеша. При этом важна детерминированность структуры.
Объекты внутри query key должны быть детерминированными. TanStack Query учитывает ключи на основе глубокого обхода, но порядок полей объекта может влиять на результат, если объект создаётся динамически.
Рекомендуемый подход:
['users', { page: 1, limit: 20 }]
Не рекомендуется полагаться на объекты, созданные из неупорядоченных источников:
const params = getParamsFromForm()
useQuery({
queryKey: ['users', params],
queryFn: fetchUsers
})
Если порядок полей в params нестабилен, возможны логические дубликаты запросов.
Для сложных приложений часто используется ручная мемоизация ключей, чтобы гарантировать их стабильность между рендерами.
Пример с useMemo:
const queryKey = useMemo(
() => ['users', { page, limit }],
[page, limit]
)
useQuery({
queryKey,
queryFn: fetchUsers
})
Это предотвращает пересоздание массива и объекта при каждом рендере, снижая нагрузку на React reconciliation, хотя не является обязательным для корректной работы кеша TanStack Query.
В крупных проектах применяется паттерн фабрик ключей:
const userKeys = {
all: ['users'],
lists: () => [...userKeys.all, 'list'],
list: (filters) => [...userKeys.lists(), filters],
detail: (id) => [...userKeys.all, 'detail', id]
}
Эта структура обеспечивает:
Пример использования:
useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId)
})
Query keys являются основой механизма invalidation. Любое несоответствие структуры ключей приводит к тому, что инвалидация не затрагивает нужные данные.
queryClient.invalidateQueries({
queryKey: ['users']
})
Этот вызов затрагивает:
Так как TanStack Query использует частичное сопоставление ключей, мемоизация работает не только на уровне полного совпадения, но и на уровне префиксов.
Query key рассматривается как путь:
['users', 'detail', 1]
Инвалидация по:
['users']
затрагивает все вложенные варианты.
Это создаёт важный принцип: мемоизация query keys одновременно работает как система маршрутизации кеша.
Основные ошибки проектирования:
queryKey: ['users', { page: Number(page) }]
Если page не стабилизирован, кеш будет раздроблен.
['users', 1] // number
['users', '1'] // string
Это два разных ключа, приводящих к дублированию данных.
['users', filters]
Если filters мутирует, мемоизация ломается логически, даже если ссылка остаётся той же.
В системах с большим количеством запросов важно минимизировать стоимость сравнения ключей и количество уникальных ключей.
Подходы:
Пример нормализации:
function normalizeFilters(filters) {
return {
page: Number(filters.page),
limit: Number(filters.limit),
sort: filters.sort ?? 'asc'
}
}
Query key фактически становится частью архитектуры состояния приложения. Его структура определяет:
Грубая структура ключей приводит к избыточному кешированию, слишком детальная — к фрагментации данных и росту числа запросов.
Баланс достигается через продуманную мемоизацию и стандартизацию ключей на уровне доменной модели.