RTK Query построен на тесной интеграции с Redux и селекторной моделью подписок. Каждый вызов хука автоматически подписывает компонент на часть состояния кеша, и именно здесь чаще всего возникают лишние ререндеры. Понимание механики подписок и стратегии сравнения данных позволяет существенно сократить количество перерисовок UI без отказа от удобства декларативного API.
Каждый вызов useQuery или useLazyQuery
создаёт подписку на конкретный ключ запроса в сторе RTK Query.
При обновлении состояния происходят два уровня проверок:
Если ссылка изменилась — компонент гарантированно ререндерится.
Даже если данные по сути эквивалентны, новая ссылка объекта приводит к обновлению UI.
RTK Query хранит данные в кеше как «результат запроса», а не как нормализованную сущность по умолчанию. Это означает, что повторная выдача идентичного массива или объекта может приводить к лишним обновлениям.
builder.query({
query: () => '/users',
})
Если сервер возвращает новый массив:
[{ id: 1, name: 'A' }]
каждый раз создаётся новая ссылка, даже при одинаковом содержимом.
Одним из ключевых инструментов является
transformResponse.
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => response,
})
Сам по себе он не решает проблему, но становится точкой внедрения стабилизации структуры.
Часто используется совместно с мемоизацией на уровне сервиса:
let cache;
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
if (cache && cache.length === response.length) return cache;
cache = response;
return response;
},
})
Это простейшая форма ручной стабилизации ссылок.
Ключевой механизм минимизации ререндеров —
selectFromResult.
Он позволяет подписываться не на весь результат, а на его часть.
const { user } = useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.find(u => u.id === 1),
}),
})
В этом случае компонент зависит только от конкретного пользователя, а не от всего массива.
Хотя selectFromResult уменьшает объём данных, он может
сам стать источником лишних ререндеров, если:
selectFromResult: ({ data }) => ({
user: data?.find(u => u.id === id),
timestamp: Date.now(),
})
Каждый вызов создаёт новый объект → ререндер.
RTK Query совместим с reselect, что позволяет
стабилизировать вычисления.
import { createSelector } from '@reduxjs/toolkit'
const selectUser = createSelector(
(res) => res.data,
(_, id) => id,
(data, id) => data?.find(u => u.id === id)
)
Использование:
const { user } = useGetUsersQuery(undefined, {
selectFromResult: (result) => ({
user: selectUser(result, 1),
}),
})
Это резко снижает количество пересчётов при неизменных входных данных.
Один из наиболее недооценённых способов оптимизации — перенос фильтрации на уровень endpoint.
const { data } = useGetUsersQuery()
const active = data?.filter(u => u.active)
Любое изменение data вызывает перерасчёт фильтра и
ререндер.
getActiveUsers: builder.query({
query: () => '/users?active=true',
})
Теперь кеш разделён, и компонент подписан только на нужный набор данных.
RTK Query автоматически инвалидирует данные через
providesTags и invalidatesTags.
Чрезмерно широкие теги приводят к каскадным обновлениям.
providesTags: ['Users']
Любое обновление пользователя инвалидирует весь список.
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Users', id })),
{ type: 'Users', id: 'LIST' },
]
: [{ type: 'Users', id: 'LIST' }]
Теперь обновляется только конкретная сущность, а не весь список.
React-Redux использует === сравнение. Любая новая ссылка
вызывает ререндер.
Типичная ошибка:
const { data } = useGetUsersQuery()
const processed = {
active: data?.filter(Boolean),
count: data?.length,
}
Каждый рендер создаёт новый объект.
const processed = useMemo(() => ({
active: data?.filter(Boolean),
count: data?.length,
}), [data])
Частая архитектурная ошибка — один компонент подписан на большой набор данных.
const { data } = useGetDashboardQuery()
Если dashboard содержит десятки сущностей, любой их
апдейт вызывает ререндер всего компонента.
Разделение endpoint-ов:
useGetUsersQuery()
useGetStatsQuery()
useGetNotificationsQuery()
Каждый компонент подписан только на свою часть кеша.
Polling увеличивает частоту обновлений, что напрямую влияет на ререндеры.
useGetMessagesQuery(undefined, {
pollingInterval: 5000,
})
Чем чаще polling, тем важнее:
selectFromResultНенужные подписки создают лишние ререндеры даже без данных.
useGetUserQuery(userId, {
skip: !userId,
})
или
useGetUserQuery(userId ?? skipToken)
Это предотвращает создание подписки и уменьшает нагрузку на store.
RTK Query использует аргументы как часть ключа кеша.
useGetUsersQuery({ page: 1, filter: 'active' })
Если объект создаётся заново:
useGetUsersQuery({ page: currentPage, filter })
и currentPage или filter пересоздаются,
возможны лишние запросы и ререндеры.
const params = useMemo(() => ({
page: currentPage,
filter,
}), [currentPage, filter])
RTK Query хранит данные по endpoint + аргументы.
Грубая гранулярность аргументов приводит к фрагментации кеша:
useGetPostsQuery({ page: 1, sort: 'asc' })
useGetPostsQuery({ page: 2, sort: 'asc' })
Каждая комбинация создаёт отдельный slice кеша.
Оптимизация:
merge в endpointmerge: (currentCache, newItems) => {
currentCache.push(...newItems)
}
Прямое чтение данных из store:
state.api.queries
приводит к лишним зависимостям.
Правильный подход:
api.endpoints.getUsers.select()
Это гарантирует стабильность структуры и минимальные пересчёты.
Эффективная работа RTK Query строится на сочетании:
selectFromResult)createSelector, useMemo)Каждый из этих элементов снижает частоту пересоздания ссылок, а значит напрямую уменьшает количество ререндеров React-компонентов, работающих поверх RTK Query.