Динамические query keys

В TanStack Query ключ запроса (query key) является фундаментальной частью системы кэширования, дедупликации и управления состоянием серверных данных. Динамические query keys используются для того, чтобы один и тот же логический запрос мог адаптироваться к входным параметрам: фильтрам, идентификаторам сущностей, пагинации, сортировке и любым другим переменным, влияющим на результат.

Ключ запроса представляет собой сериализуемую структуру, чаще всего массив, который однозначно идентифицирует данные в кэше. Именно изменение ключа запускает новый запрос и приводит к созданию отдельной записи в кэше.


Базовая структура query key

На практике query key почти всегда оформляется в виде массива:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Такой ключ статичен и подходит только для неизменяемых наборов данных. Любое расширение логики требует введения динамических элементов:

useQuery({
  queryKey: ['users', userId],
  queryFn: () => fetchUser(userId)
})

Здесь userId превращает один логический запрос в множество независимых кэш-записей.


Принцип изоляции данных через ключи

Каждое изменение значения внутри query key формирует отдельную сущность кэша. Это означает, что:

  • ['users', 1] и ['users', 2] — разные записи
  • TanStack Query не смешивает результаты
  • кеширование работает на уровне точного совпадения структуры

Это поведение делает query key основным инструментом сегментации данных.


Динамические параметры как часть ключа

Наиболее частые сценарии использования динамических ключей:

Идентификаторы сущностей

useQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId)
})

Любое изменение postId приводит к новому запросу и независимому кэшу.


Фильтры и параметры поиска

useQuery({
  queryKey: ['products', { category, priceRange, sort }],
  queryFn: () => fetchProducts({ category, priceRange, sort })
})

Объекты внутри ключа допустимы, но требуют строгой стабильности структуры.

Важно, что TanStack Query сравнивает ключи глубоко, но не защищает от нестабильных ссылок, если объект создаётся заново на каждом рендере.


Пагинация

useQuery({
  queryKey: ['orders', { page, limit }],
  queryFn: () => fetchOrders({ page, limit })
})

Каждая страница становится отдельным кэшем, если не используется infinite query.


Стабильность query keys

Ключевой аспект динамических query keys — их стабильность. Даже если данные логически одинаковые, разные ссылки объектов могут привести к различным ключам.

Нестабильный вариант:

queryKey: ['users', { page: 1 }]

при каждом рендере создаёт новый объект. Несмотря на глубокое сравнение, подобная практика усложняет предсказуемость и может влиять на инструменты девтулов и рефетчинг.

Стабильный вариант:

const key = ['users', page]

useQuery({
  queryKey: key,
  queryFn: () => fetchUsers(page)
})

Query key factory pattern

Для масштабируемых приложений применяется фабрика ключей. Это позволяет унифицировать структуру кэша и избегать ошибок.

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)
})

Преимущества подхода:

  • централизованная структура ключей
  • предсказуемая иерархия
  • упрощённая инвалидация
  • снижение дублирования логики

Влияние ключей на инвалидацию

Инвалидация работает по частичному совпадению ключей:

queryClient.invalidateQueries({
  queryKey: ['users']
})

Это затронет:

  • ['users']
  • ['users', 1]
  • ['users', { filters }]

Иерархическая структура ключей позволяет управлять группами данных.


Частые ошибки при работе с динамическими ключами

Использование нестабильных объектов

queryKey: ['data', { filter: getFilter() }]

Если getFilter() возвращает новый объект каждый раз, ключи становятся трудно предсказуемыми.


Включение не сериализуемых значений

Нежелательно использовать:

  • функции
  • классы
  • DOM-элементы
  • Map / Set без преобразования
queryKey: ['data', new Date()]

Даже если это допустимо технически, ключ теряет предсказуемость.


Смешивание уровней абстракции

Плохая практика:

['users', user, settings, true, 42]

Такой ключ сложно поддерживать и инвалидация становится неконтролируемой.


Динамические ключи и кэширование

TanStack Query использует query key как индекс кэша. Это означает:

  • одинаковые ключи → общий кэш
  • разные ключи → разные записи
  • порядок элементов имеет значение
['users', 1] !== ['users', '1']

Строгая типизация структуры ключа становится критически важной.


Композиция ключей для сложных запросов

При сложных доменных моделях ключи формируются композиционно:

const commentKeys = {
  all: ['comments'],
  byPost: (postId) => [...commentKeys.all, 'post', postId],
  byUser: (userId) => [...commentKeys.all, 'user', userId]
}

Такой подход позволяет строить масштабируемую систему кэширования, где каждый уровень домена имеет собственное пространство ключей.


Влияние порядка элементов

Порядок элементов в массиве query key строго влияет на идентичность:

['users', 1, 'profile'] !== ['users', 'profile', 1]

Это свойство используется для строгой сегментации запросов, но требует дисциплины при проектировании структуры ключей.


Практика построения ключей в реальных приложениях

В прикладных системах ключи часто отражают REST-структуру:

  • список ресурсов
  • детальная сущность
  • связанные данные

Пример:

const postKeys = {
  all: ['posts'],
  lists: () => [...postKeys.all, 'list'],
  list: (filters) => [...postKeys.lists(), filters],
  detail: (id) => [...postKeys.all, 'detail', id],
  comments: (id) => [...postKeys.detail(id), 'comments']
}

Такая структура формирует дерево данных, которое напрямую отражает зависимость сущностей.


Роль query keys в дедупликации запросов

TanStack Query предотвращает повторные запросы, если ключ и параметры запроса совпадают. Динамические ключи позволяют:

  • объединять одинаковые запросы
  • разделять разные состояния одного ресурса
  • переиспользовать данные между компонентами

Это делает query key центральным элементом оптимизации сетевого слоя приложения.