Мемоизация query keys

В 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 преобразуется в стабильное строковое или хеш-подобное представление. Это позволяет использовать его как индекс в кеше.

Упрощённо процесс выглядит так:

  1. Query key передаётся в хранилище
  2. Производится глубокая нормализация структуры
  3. Полученный результат используется как уникальный идентификатор
  4. По этому идентификатору выполняется доступ к кешу

Важно, что порядок элементов массива имеет значение:

['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 нестабилен, возможны логические дубликаты запросов.


Мемоизация query key на уровне приложения

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

Пример с useMemo:

const queryKey = useMemo(
  () => ['users', { page, limit }],
  [page, limit]
)

useQuery({
  queryKey,
  queryFn: fetchUsers
})

Это предотвращает пересоздание массива и объекта при каждом рендере, снижая нагрузку на React reconciliation, хотя не является обязательным для корректной работы кеша TanStack Query.


Фабрики query keys и централизованная мемоизация

В крупных проектах применяется паттерн фабрик ключей:

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

Этот вызов затрагивает:

  • [‘users’]
  • [‘users’, 1]
  • [‘users’, ‘active’]
  • [‘users’, { page: 1 }]

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


Частичное совпадение и префиксная модель

Query key рассматривается как путь:

['users', 'detail', 1]

Инвалидация по:

['users']

затрагивает все вложенные варианты.

Это создаёт важный принцип: мемоизация query keys одновременно работает как система маршрутизации кеша.


Проблемы неправильной мемоизации ключей

Основные ошибки проектирования:

1. Пересоздание структур без необходимости

queryKey: ['users', { page: Number(page) }]

Если page не стабилизирован, кеш будет раздроблен.

2. Смешивание типов

['users', 1] // number
['users', '1'] // string

Это два разных ключа, приводящих к дублированию данных.

3. Неявные зависимости

['users', filters]

Если filters мутирует, мемоизация ломается логически, даже если ссылка остаётся той же.


Оптимизация мемоизации в высоконагруженных сценариях

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

Подходы:

  • нормализация параметров до передачи в query key
  • использование стабильных DTO-структур
  • выделение вычисляемых ключей вне компонентов
  • ограничение глубины вложенности объектов

Пример нормализации:

function normalizeFilters(filters) {
  return {
    page: Number(filters.page),
    limit: Number(filters.limit),
    sort: filters.sort ?? 'asc'
  }
}

Связь мемоизации query keys и архитектуры данных

Query key фактически становится частью архитектуры состояния приложения. Его структура определяет:

  • гранулярность кеша
  • стратегию обновления данных
  • поведение prefetching
  • точность invalidation

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

Баланс достигается через продуманную мемоизацию и стандартизацию ключей на уровне доменной модели.