Query filters и pattern matching

Система идентификации запросов через queryKey

В основе работы TanStack Query лежит queryKey — структурированный идентификатор запроса. Именно он используется для группировки, поиска и управления кешированными данными.

queryKey может быть как примитивом, так и массивом:

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

useQuery({
  queryKey: ['users', 1],
  queryFn: () => fetchUserById(1),
})

useQuery({
  queryKey: ['users', 1, 'posts'],
  queryFn: () => fetchUserPosts(1),
})

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


Query Filters как механизм выборки

Query filters — это объект, который описывает условия поиска запросов внутри QueryClient. Они используются во всех методах, работающих с множеством запросов:

  • queryClient.getQueriesData
  • queryClient.setQueriesData
  • queryClient.invalidateQueries
  • queryClient.removeQueries
  • queryClient.refetchQueries

Базовая структура фильтра:

const filter = {
  queryKey: [],
  exact: false,
  type: 'active' | 'inactive' | 'all',
  stale: boolean,
  fetchStatus: 'fetching' | 'paused' | 'idle',
}

На практике чаще всего используются только queryKey и exact.


Поведение exact и частичное совпадение

Ключевой механизм pattern matching — параметр exact.

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

exact: true

Только полное совпадение ключа:

queryKey: ['users']  // совпадает
queryKey: ['users', 1] // не совпадает

exact: false (по умолчанию)

Работает как префиксный матчинг:

queryKey: ['users'] совпадает с:
['users']
['users', 1]
['users', 1, 'posts']

Это базовый механизм иерархической фильтрации.


Префиксное сопоставление queryKey

TanStack Query рассматривает queryKey как дерево, где массив — путь:

['users'] 
  ├── ['users', 1]
  │     ├── ['users', 1, 'posts']
  │     └── ['users', 1, 'followers']
  └── ['users', 2]

Запрос:

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

затронет:

  • [‘users’, 1]
  • [‘users’, 1, ‘posts’]
  • [‘users’, 1, ‘followers’]

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

  • [‘users’, 2]

Глубокая фильтрация через массивы ключей

Pattern matching работает не только на первом уровне, но и на каждом элементе массива.

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

Это уже более узкий фильтр, который:

  • совпадает с точным запросом
  • не затрагивает ['users', 1, 'followers']

Механизм сравнения идёт поэлементно:

  1. сравнивается users
  2. сравнивается 1
  3. сравнивается posts

Любое расхождение прерывает матчинг, если exact: true.


Использование partial matching в реальных сценариях

Инвалидация всех пользовательских данных

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

Используется после мутаций:

await updateUser(data)

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

Это гарантирует синхронизацию всего пользовательского кэша.


Обновление только конкретного пользователя

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

Это ограничивает влияние:

  • профиль пользователя
  • связанные сущности (если вложены в key)

Точечная очистка кэша

queryClient.removeQueries({
  queryKey: ['users', userId],
})

Удаляет данные из кеша полностью, включая состояние и метаданные.


Role of predicate-based filters

Помимо queryKey, TanStack Query поддерживает функциональные фильтры через predicate.

queryClient.invalidateQueries({
  predicate: (query) => {
    return query.queryKey[0] === 'users'
  },
})

Это расширяет pattern matching до произвольной логики.


Сравнение queryKey matching и predicate

queryKey matching

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

Подходит для:

  • структурированных данных
  • предсказуемых ключей
  • иерархий

predicate

queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey.includes('users') &&
    query.state.dataUpdatedAt < Date.now() - 1000 * 60 * 5,
})

Подходит для:

  • сложных условий
  • временных фильтров
  • состояния запроса

Сопоставление с type, stale и fetchStatus

Query filters могут комбинироваться с состоянием запроса:

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

type

  • active — только используемые в UI запросы
  • inactive — неиспользуемые
  • all — все

stale

queryClient.refetchQueries({
  stale: true,
})

Позволяет работать только с устаревшими данными.


fetchStatus

queryClient.refetchQueries({
  fetchStatus: 'idle',
})

Фильтрует запросы по состоянию выполнения:

  • fetching
  • paused
  • idle

Комбинирование фильтров

Все параметры фильтра работают совместно:

queryClient.invalidateQueries({
  queryKey: ['users'],
  type: 'active',
  stale: true,
})

Логика:

  1. сначала выбираются запросы по ключу
  2. затем применяется фильтрация по типу
  3. затем по состоянию stale/fetchStatus

Pattern matching в setQueriesData

Фильтры используются не только для инвалидирования, но и для прямого изменения кеша:

queryClient.setQueriesData(
  {
    queryKey: ['users'],
  },
  (oldData) => {
    return oldData?.map(user =>
      user.id === 1 ? { ...user, name: 'Upd ated' } : user
    )
  }
)

Это позволяет массово обновлять данные без повторного запроса.


Частичное совпадение с разными уровнями вложенности

Структура ключей может быть глубокой:

['org', orgId, 'users', userId, 'posts', postId]

Фильтр:

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

Затронет:

  • все пользователи организации
  • все посты пользователей
  • любые вложенные сущности

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

  • другие организации

Оптимизация через точечные фильтры

Чем более точный queryKey, тем меньше лишних перезапросов:

queryClient.invalidateQueries({
  queryKey: ['users', userId, 'profile'],
  exact: true,
})

Такой подход уменьшает:

  • сетевую нагрузку
  • количество rerender’ов
  • лишние refetch’и

Антипаттерны использования фильтров

Слишком общие ключи

queryKey: ['data']

Проблема:

  • невозможно эффективно фильтровать
  • invalidateQueries становится слишком дорогим

Использование только predicate без структуры

predicate: (q) => q.queryKey.toString().includes('user')

Проблема:

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

Чрезмерная вложенность без необходимости

['a', 'b', 'c', 'd', 'e', 'f']

Проблема:

  • сложность фильтрации без реальной пользы
  • ухудшение читаемости архитектуры кэша

Модель мышления при проектировании queryKey

queryKey следует рассматривать как адрес:

resource → entity → subresource → detail

Примеры:

['users']
['users', userId]
['users', userId, 'posts']
['users', userId, 'posts', postId]

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


Взаимодействие фильтров с кешем QueryClient

Query filters напрямую воздействуют на внутренний реестр QueryClient:

  • поиск query-объектов
  • фильтрация по метаданным
  • выполнение операций batch-обновления

Каждый вызов:

queryClient.invalidateQueries(...)

фактически проходит через:

  1. обход всех зарегистрированных queries
  2. применение фильтра
  3. выполнение операции над совпавшими элементами

Поведение при отсутствии совпадений

Если фильтр не находит совпадений:

  • операция завершается без ошибок
  • кеш остаётся неизменным
  • UI не триггерит rerender

Это делает систему безопасной для массовых операций.


Стабильность pattern matching при динамических ключах

Динамические ключи:

queryKey: ['users', userId, tab]

Фильтр:

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

Работает корректно независимо от tab, так как сравнение идёт по префиксу.


Роль порядка элементов в queryKey

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

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

Это означает, что pattern matching не является наборным (se t-based), а позиционным.


Практическая модель фильтрации в приложениях

В реальных архитектурах чаще всего используется комбинация:

  • префиксные queryKey
  • точечные invalidateQueries
  • predicate для сложных условий

Типовая схема:

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

и

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

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