Диагностика ререндеров

Поведение ререндеров в React-приложениях с TanStack Query напрямую связано с моделью подписок на состояние кэша. Каждый вызов useQuery формирует подписку на конкретный queryKey, а любое изменение состояния, связанного с этим ключом, приводит к обновлению всех подписчиков.

Ключевой момент: ререндер не всегда означает сетевой запрос. Обновление может быть вызвано изменением статуса запроса, данных, метаданных или даже внутренних оптимизаций React Query.


Подписочная модель и точки обновления

TanStack Query работает как реактивный слой над кэшем. Компонент, использующий:

const { data, isFetching, status } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

подписывается на следующие изменения:

  • изменение data
  • изменение isFetching, isLoading, isError, status
  • изменение fetchStatus
  • инвалидирование или перезапрос по queryKey
  • изменения через queryClient.setQueryData

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


Структурное шарингирование и его влияние

Одним из ключевых механизмов оптимизации является structural sharing — попытка сохранить ссылочную стабильность данных между обновлениями.

Если сервер возвращает логически идентичные данные, но с новой ссылкой, TanStack Query пытается:

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

Однако при отключении или обходе structural sharing (например, при кастомной трансформации данных) ререндеры становятся более частыми, так как меняются ссылки:

select: (data) => data.map(item => ({
  ...item,
  computed: heavyCompute(item),
}))

Каждый вызов создаёт новые ссылки, даже если исходные данные не изменились.


Роль queryKey в частоте обновлений

queryKey является базовым идентификатором подписки. Изменение ключа означает полную смену подписки.

Проблемы диагностики ререндеров часто связаны с нестабильными ключами:

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

Если filter создаётся как новый объект при каждом рендере:

const filter = { role: 'admin' }

то происходит:

  • пересоздание queryKey
  • новая подписка
  • повторный fetch
  • ререндер

Стабильность ключа критична для контроля обновлений.


Факторы, вызывающие лишние ререндеры

1. Изменение referential identity

Любое изменение ссылки данных, возвращаемых через select, placeholderData, initialData, приводит к обновлению компонента.

2. Частые состояния fetching

isFetching обновляется при каждом фоне обновления данных. Даже если data не меняется, компонент может перерендериться.

3. Автоматические refetch-стратегии

  • refetchOnWindowFocus
  • refetchOnReconnect
  • refetchInterval

Эти механизмы создают скрытые источники ререндеров.

4. Инвалидация кэша

queryClient.invalidateQueries(['users'])

Приводит к переходу запроса в состояние stale → fetch → обновление подписчиков.


Инструменты диагностики

React DevTools Profiler

Основной инструмент анализа:

  • фиксируется количество ререндеров
  • измеряется длительность commit-фазы
  • отслеживаются причины обновлений

Особое внимание требуется на:

  • изменения props компонента
  • изменения значения data
  • обновления контекста QueryClient

TanStack Query Devtools

Devtools позволяют наблюдать:

  • статус запроса (fresh, stale, fetching)
  • момент инвалидирования
  • количество активных подписчиков
  • изменения cache-time и gc-time

Часто выявляются скрытые refetch-петли, вызванные неправильными ключами или агрессивной стратегией staleTime.


Оптимизация через select и мемоизацию

select позволяет уменьшить поверхность ререндеров, если используется корректно:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (data) => data.filter(u => u.active),
})

Но при отсутствии мемоизации select становится источником лишних ререндеров.

Для стабилизации применяется:

  • мемоизация вычислений
  • вынесение тяжелой логики наружу
  • использование useCallback / useMemo в связке с результатами query

Placeholder data и initial data как источники обновлений

placeholderData создаёт временное состояние до загрузки реальных данных:

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

При переходе от placeholder к реальным данным происходит гарантированный ререндер.

initialData ведёт себя аналогично, но считается уже частью кэша и может влиять на hydration-логику.


Keep previous data и сглаживание переходов

Опция keepPreviousData уменьшает визуальные скачки состояния, но не устраняет ререндеры:

  • старые данные остаются в data
  • обновляется isFetching
  • компонент перерендеривается при каждом refetch

Подписка на глобальные состояния fetch/mutation

Дополнительные источники ререндеров:

useIsFetching()
useIsMutating()

Любое изменение глобального счётчика активных запросов вызывает обновление всех подписчиков.

Даже если локальные данные не меняются, изменение глобального fetch-state приводит к ререндеру.


QueryClient.setQueryData и синтетические обновления

Прямое обновление кэша:

queryClient.setQueryData(['users'], (old) => [...old, newUser])

Вызывает:

  • немедленное обновление подписчиков
  • ререндер всех компонентов, использующих ключ

При частых вызовах может возникать каскад обновлений.


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

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

queryClient.invalidateQueries()

запускает цепочку:

  1. пометка данных как stale
  2. триггер refetch
  3. обновление cache
  4. уведомление подписчиков
  5. ререндер компонентов

При широких селекторах (invalidateQueries без ключа) эффект усиливается многократно.


Suspense-режим и влияние на рендеринг

В режиме Suspense ререндеры проявляются иначе:

  • компонент не отображает loading-state
  • происходит повторное монтирование после загрузки
  • любые изменения данных приводят к повторному commit

Это усложняет диагностику, так как классические флаги isLoading отсутствуют.


Стабилизация компонентов через разбиение подписок

Частая причина лишних ререндеров — подписка на слишком широкий набор данных:

const { data } = useQuery(...)

вместо этого:

const { data } = useQuery({
  select: (data) => data.users,
})

или разделение запросов:

  • отдельный query для списка
  • отдельный query для метаданных
  • отдельный query для фильтров

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


Типичные паттерны источников лишних обновлений

  • нестабильные queryKey с объектами
  • отсутствующий select при тяжёлых данных
  • частые background refetch (staleTime = 0)
  • глобальные подписки useIsFetching
  • массовая инвалидation
  • отсутствие структурной стабильности данных
  • вычисления внутри render без мемоизации результата query

Поведенческая модель ререндеров

Ререндер в TanStack Query определяется не одним фактором, а комбинацией:

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

Контроль достигается не подавлением ререндеров, а управлением источниками сигналов обновления и границами подписок.