Поведение ререндеров в React-приложениях с TanStack Query напрямую
связано с моделью подписок на состояние кэша. Каждый вызов
useQuery формирует подписку на конкретный
queryKey, а любое изменение состояния, связанного с этим
ключом, приводит к обновлению всех подписчиков.
Ключевой момент: ререндер не всегда означает сетевой запрос. Обновление может быть вызвано изменением статуса запроса, данных, метаданных или даже внутренних оптимизаций React Query.
TanStack Query работает как реактивный слой над кэшем. Компонент, использующий:
const { data, isFetching, status } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
подписывается на следующие изменения:
dataisFetching, isLoading,
isError, statusfetchStatusqueryKeyqueryClient.setQueryDataЛюбое из этих событий вызывает уведомление подписчиков и потенциальный ререндер.
Одним из ключевых механизмов оптимизации является structural sharing — попытка сохранить ссылочную стабильность данных между обновлениями.
Если сервер возвращает логически идентичные данные, но с новой ссылкой, TanStack Query пытается:
Однако при отключении или обходе structural sharing (например, при кастомной трансформации данных) ререндеры становятся более частыми, так как меняются ссылки:
select: (data) => data.map(item => ({
...item,
computed: heavyCompute(item),
}))
Каждый вызов создаёт новые ссылки, даже если исходные данные не изменились.
queryKey является базовым идентификатором подписки.
Изменение ключа означает полную смену подписки.
Проблемы диагностики ререндеров часто связаны с нестабильными ключами:
useQuery({
queryKey: ['users', filter],
})
Если filter создаётся как новый объект при каждом
рендере:
const filter = { role: 'admin' }
то происходит:
queryKeyСтабильность ключа критична для контроля обновлений.
Любое изменение ссылки данных, возвращаемых через
select, placeholderData,
initialData, приводит к обновлению компонента.
isFetching обновляется при каждом фоне обновления
данных. Даже если data не меняется, компонент может
перерендериться.
refetchOnWindowFocusrefetchOnReconnectrefetchIntervalЭти механизмы создают скрытые источники ререндеров.
queryClient.invalidateQueries(['users'])
Приводит к переходу запроса в состояние stale → fetch → обновление подписчиков.
Основной инструмент анализа:
Особое внимание требуется на:
dataDevtools позволяют наблюдать:
fresh, stale,
fetching)Часто выявляются скрытые refetch-петли, вызванные неправильными ключами или агрессивной стратегией staleTime.
select позволяет уменьшить поверхность ререндеров, если
используется корректно:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.filter(u => u.active),
})
Но при отсутствии мемоизации select становится
источником лишних ререндеров.
Для стабилизации применяется:
useCallback / useMemo в
связке с результатами queryplaceholderData создаёт временное состояние до загрузки
реальных данных:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
placeholderData: [],
})
При переходе от placeholder к реальным данным происходит гарантированный ререндер.
initialData ведёт себя аналогично, но считается уже
частью кэша и может влиять на hydration-логику.
Опция keepPreviousData уменьшает визуальные скачки
состояния, но не устраняет ререндеры:
dataisFetchingДополнительные источники ререндеров:
useIsFetching()
useIsMutating()
Любое изменение глобального счётчика активных запросов вызывает обновление всех подписчиков.
Даже если локальные данные не меняются, изменение глобального fetch-state приводит к ререндеру.
Прямое обновление кэша:
queryClient.setQueryData(['users'], (old) => [...old, newUser])
Вызывает:
При частых вызовах может возникать каскад обновлений.
Инвалидация:
queryClient.invalidateQueries()
запускает цепочку:
При широких селекторах (invalidateQueries без ключа)
эффект усиливается многократно.
В режиме Suspense ререндеры проявляются иначе:
Это усложняет диагностику, так как классические флаги
isLoading отсутствуют.
Частая причина лишних ререндеров — подписка на слишком широкий набор данных:
const { data } = useQuery(...)
вместо этого:
const { data } = useQuery({
select: (data) => data.users,
})
или разделение запросов:
Чем уже подписка, тем меньше область ререндеров.
queryKey с объектамиselect при тяжёлых данныхuseIsFetchingРерендер в TanStack Query определяется не одним фактором, а комбинацией:
Контроль достигается не подавлением ререндеров, а управлением источниками сигналов обновления и границами подписок.