RTK Query строит работу вокруг автоматического кэширования данных. После выполнения запроса результат сохраняется во внутреннем store Redux и может повторно использоваться без дополнительного обращения к серверу. Это снижает количество HTTP-запросов, уменьшает нагрузку на backend и ускоряет отображение интерфейса.
Кэш в RTK Query обладает несколькими особенностями:
Грамотное использование кэша напрямую влияет на производительность приложения.
После первого запроса данные помещаются в store:
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
})
})
})
При использовании:
const { data } = useGetPostsQuery()
RTK Query:
Если другой компонент вызывает тот же hook:
const { data } = useGetPostsQuery()
новый HTTP-запрос не выполняется. Используется уже существующий cache entry.
RTK Query формирует уникальный ключ кэша на основе:
Пример:
useGetPostQuery(1)
useGetPostQuery(2)
Создаются разные cache entries:
getPost(1)
getPost(2)
Это позволяет независимо хранить данные.
Аргументы query сериализуются автоматически.
Пример:
useGetPostsQuery({
page: 1,
limit: 10
})
RTK Query создаёт сериализованный ключ:
getPosts({"page":1,"limit":10})
Если структура объекта одинакова, кэш будет переиспользован.
Проблема:
useGetPostsQuery({
page: currentPage,
timestamp: Date.now()
})
timestamp постоянно меняется, поэтому RTK Query считает
запрос новым.
Результат:
Правильный подход:
useGetPostsQuery({
page: currentPage
})
По умолчанию RTK Query удаляет данные из кэша через 60 секунд после исчезновения последнего подписчика.
Настройка:
getPosts: builder.query({
query: () => '/posts',
keepUnusedDataFor: 300
})
Здесь кэш хранится 5 минут.
Подходит для:
Подходит для:
Позволяет повторно загружать данные даже при наличии кэша.
useGetPostsQuery(undefined, {
refetchOnMountOrArgChange: true
})
Поведение:
Можно задать число:
useGetPostsQuery(undefined, {
refetchOnMountOrArgChange: 30
})
Это означает:
Такой режим особенно полезен для компромисса между свежестью и производительностью.
RTK Query умеет автоматически обновлять данные после возвращения пользователя к вкладке браузера.
const api = createApi({
baseQuery,
refetchOnFocus: true,
endpoints: () => ({})
})
Поведение:
Это удобно для:
Автоматический refetch после восстановления сети:
const api = createApi({
baseQuery,
refetchOnReconnect: true,
endpoints: () => ({})
})
Сценарий:
Polling позволяет периодически обновлять cache.
useGetNotificationsQuery(undefined, {
pollingInterval: 5000
})
Запрос выполняется каждые 5 секунд.
Важно понимать:
Одно из ключевых средств оптимизации ререндеров.
Проблема:
const { data } = useGetPostsQuery()
Компонент будет ререндериться при любом изменении массива.
Оптимизация:
const { post } = useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find(post => post.id === 5)
})
})
Теперь ререндер происходит только при изменении выбранного объекта.
Плохой вариант:
const { data } = useGetPostsQuery()
Если в массиве изменился один элемент — обновится весь компонент.
Лучший вариант:
const { item } = useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
item: data?.[0]
})
})
RTK Query сравнивает результаты selector и избегает лишних обновлений.
Крупные массивы хуже подходят для точечных обновлений.
Плохой подход:
[
{ id: 1, name: 'A' },
{ id: 2, name: 'B' }
]
Лучший вариант — normalized state:
{
ids: [1, 2],
entities: {
1: { id: 1, name: 'A' },
2: { id: 2, name: 'B' }
}
}
RTK Query хорошо работает вместе с
createEntityAdapter.
Пример:
const postsAdapter = createEntityAdapter()
const initialState = postsAdapter.getInitialState()
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
return postsAdapter.setAll(initialState, response)
}
})
Преимущества:
Не стоит хранить в кэше сырые данные сервера, если они неудобны.
Пример:
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
return response.items
}
})
Или:
transformResponse: (response) => {
return response.map(post => ({
...post,
fullName: `${post.author} (${post.id})`
}))
}
Подготовка данных на этапе кэширования уменьшает вычисления в компонентах.
Плохой вариант:
const { data } = useGetPostsQuery()
const filtered = data?.filter(post => post.active)
Фильтрация выполняется на каждом рендере.
Лучше:
transformResponse: (response) => {
return response.filter(post => post.active)
}
RTK Query позволяет обновлять cache вручную без повторного запроса.
Пример optimistic update:
dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(newPost)
}
)
)
Это особенно эффективно:
Позволяет применять частичные изменения.
dispatch(
api.util.patchQueryData(
'getPost',
5,
(draft) => {
draft.title = 'Updated'
}
)
)
Преимущество — отсутствие полного refetch.
Инвалидация — основной механизм синхронизации кэша.
Пример query:
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts']
})
Mutation:
addPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
}),
invalidatesTags: ['Posts']
})
После mutation RTK Query:
Глобальная инвалидизация может быть дорогой.
Плохо:
invalidatesTags: ['Posts']
Лучше:
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: 'Posts',
id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }]
Mutation:
invalidatesTags: (result, error, id) => [
{ type: 'Posts', id }
]
Обновляется только конкретная запись.
RTK Query поддерживает prefetch.
dispatch(
api.util.prefetch(
'getPosts',
undefined,
{
force: true
}
)
)
Полезно:
Иногда не требуется автоматическая загрузка.
const [trigger, result] = useLazyGetPostsQuery()
Запрос выполняется только вручную:
trigger()
Это снижает количество ненужных cache entries.
Можно полностью отключить query.
const { data } = useGetPostQuery(id, {
skip: !id
})
Без skip RTK Query может создавать лишние запросы.
Более безопасный вариант:
import { skipToken } from '@reduxjs/toolkit/query'
const result = useGetPostQuery(
id ?? skipToken
)
Особенно полезно с TypeScript.
Плохо:
getDashboardData
Возвращает:
Минусы:
Лучше:
getProfile
getNotifications
getStats
getSettings
Небольшие cache entries легче обновлять и переиспользовать.
RTK Query поддерживает websocket-интеграции через
onCacheEntryAdded.
Пример:
getMessages: builder.query({
query: () => '/messages',
async onCacheEntryAdded(
arg,
{
updateCachedData,
cacheDataLoaded,
cacheEntryRemoved
}
) {
const socket = new WebSocket('ws://localhost')
await cacheDataLoaded
socket.onmess age = (event) => {
const message = JSON.parse(event.data)
updateCachedData((draft) => {
draft.push(message)
})
}
await cacheEntryRemoved
socket.close()
}
})
Преимущества:
RTK Query автоматически объединяет одинаковые запросы.
Если одновременно вызываются:
useGetPostsQuery()
useGetPostsQuery()
useGetPostsQuery()
выполняется только один HTTP-запрос.
Остальные компоненты подписываются на существующий cache entry.
Слишком большое время хранения кэша может привести к:
Особенно опасно:
keepUnusedDataFor: 36000
для крупных datasets.
Не все данные требуют одинаковой свежести.
Пример:
pollingInterval: 3000
refetchOnFocus: true
Подходит для:
keepUnusedDataFor: 3600
Подходит для:
При пагинации важно избегать смешивания страниц.
Правильно:
useGetPostsQuery({
page: 1
})
Каждая страница получает отдельный cache key.
RTK Query позволяет объединять страницы.
Пример:
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
},
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
}
Так формируется единый cache entry.
Позволяет вручную определить условия обновления.
forceRefetch({
currentArg,
previousArg
}) {
return currentArg.page !== previousArg.page
}
Это полезно для сложной пагинации и infinite scroll.
При Server-Side Rendering RTK Query умеет:
Это особенно важно для:
Полная очистка кэша:
dispatch(api.util.resetApiState())
Обычно используется:
Небольшие cache entries эффективнее.
Точная invalidation значительно снижает refetch.
Это один из главных инструментов оптимизации.
Постоянные запросы быстро перегружают backend.
Это уменьшает необходимость refetch.
Особенно для крупных коллекций.
Иначе кэширование теряет смысл.
Разным данным нужна разная стратегия хранения.