RTK Query представляет собой надстройку над Redux Toolkit, предназначенную для организации слоя работы с серверными данными. Архитектура библиотеки строится вокруг декларативного описания API, автоматического кэширования, генерации хуков и централизованного управления состоянием запросов.
В основе архитектуры лежат несколько ключевых компонентов:
Главная особенность архитектуры RTK Query заключается в том, что серверное состояние рассматривается как отдельный слой приложения, независимый от UI и локального client-side state.
Классический Redux исторически использовался для хранения любого состояния приложения:
RTK Query меняет подход к архитектуре и отделяет:
Локальное состояние интерфейса:
{
theme: 'dark',
modalOpen: true,
sidebarCollapsed: false
}
Данные, приходящие с сервера:
{
users: [...],
posts: [...],
comments: [...]
}
Server state обладает особенностями:
RTK Query полностью берет управление этим слоем на себя.
Архитура RTK Query строится вокруг createApi.
Именно API Slice становится единым контейнером для:
Базовая структура:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
reducerPathreducerPath определяет namespace внутри Redux Store.
Пример:
reducerPath: 'api'
В store появится:
{
api: {
queries: {},
mutations: {},
subscriptions: {},
provided: {}
}
}
Это изолирует внутреннюю инфраструктуру RTK Query от остального Redux-состояния.
API Slice содержит несколько внутренних подсистем.
Хранение query-кэша:
state.api.queries
Пример:
{
'getUsers(undefined)': {
status: 'fulfilled',
data: [...],
fulfilledTimeStamp: 123456
}
}
Хранение состояния mutation-запросов:
state.api.mutations
Система активных подписок компонентов:
state.api.subscriptions
RTK Query отслеживает:
Система связей между query и mutation:
state.api.provided
Используется для автоматической инвалидации.
Архитектура RTK Query отделяет транспортный слой от endpoint-ов.
Эта задача решается через baseQuery.
fetchBaseQueryСтандартная реализация поверх Fetch API:
baseQuery: fetchBaseQuery({
baseUrl: '/api'
})
Она отвечает за:
baseQueryEndpoints не знают:
Endpoints описывают только:
query: () => '/users'
Весь транспорт инкапсулирован.
Архитектура позволяет заменить транспортный слой полностью.
Пример с Axios:
const axiosBaseQuery =
({ baseUrl }) =>
async ({ url, method, data }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data
})
return { data: result.data }
} catch (error) {
return {
error: {
status: error.response?.status,
data: error.response?.data
}
}
}
}
Endpoints являются декларативным описанием API.
RTK Query разделяет endpoints на два типа:
Query предназначен для чтения данных.
getUsers: builder.query({
query: () => '/users'
})
Архитурно query включает:
Mutation предназначен для изменения данных.
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body
})
})
Mutation архитектурно отличается:
RTK Query использует builder API.
endpoints: (builder) => ({
getUsers: builder.query(...),
createUser: builder.mutation(...)
})
Builder обеспечивает:
Кэш — центральная часть RTK Query.
Каждый query получает уникальный cache key.
Пример:
useGetUserQuery(5)
Создает ключ:
'getUser(5)'
Если другой компонент вызывает тот же query:
useGetUserQuery(5)
RTK Query не выполняет новый запрос.
Используется существующий cache entry.
RTK Query автоматически предотвращает дублирующие запросы.
Если одновременно вызываются:
useGetUsersQuery()
в нескольких компонентах:
<UserList />
<UserSidebar />
<UserStats />
будет выполнен только один HTTP-запрос.
Все компоненты подпишутся на общий cache entry.
Архитектура кэша построена на подписках.
Когда компонент монтируется:
useGetUsersQuery()
создается subscription.
Когда компонент размонтируется — subscription удаляется.
После удаления последнего подписчика RTK Query не очищает кэш мгновенно.
Используется таймер:
keepUnusedDataFor: 60
Данные сохраняются:
60 секунд
Это уменьшает повторные запросы.
RTK Query реализует автоматическую очистку кэша.
Процесс:
Это предотвращает утечки памяти.
Одно из главных преимуществ RTK Query — tag-based invalidation.
Query может предоставлять tags:
getUsers: builder.query({
query: () => '/users',
providesTags: ['Users']
})
Mutation может инвалидировать их:
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body
}),
invalidatesTags: ['Users']
})
После успешной mutation:
invalidatesTags: ['Users']
RTK Query:
UsersАрхитектура поддерживает granular invalidation.
providesTags: (result, error, id) => [
{ type: 'Users', id }
]
Mutation:
invalidatesTags: (result, error, id) => [
{ type: 'Users', id }
]
Это позволяет обновлять только конкретный entity cache.
RTK Query использует document cache architecture.
Это означает:
getUsers()
getUser(id)
хранятся независимо.
RTK Query не выполняет глобальную нормализацию автоматически.
Полная normalization architecture:
RTK Query делает ставку на:
Каждый query проходит lifecycle.
Этапы:
uninitialized
pending
fulfilled
rejected
Mutation имеет похожий lifecycle:
uninitialized
pending
fulfilled
rejected
RTK Query хранит:
{
isLoading,
isFetching,
isSuccess,
isError,
error,
data
}
isLoading и isFetchingПервый запрос без данных.
Повторный запрос при наличии cache data.
Это архитектурно важно для UX.
RTK Query поддерживает несколько стратегий обновления.
refetchOnMountOrArgChange: true
refetchOnFocus: true
При возвращении во вкладку данные обновляются автоматически.
refetchOnReconnect: true
После восстановления сети запросы повторяются.
RTK Query поддерживает polling.
useGetUsersQuery(undefined, {
pollingInterval: 5000
})
Запрос выполняется каждые 5 секунд.
RTK Query создает собственный middleware.
Подключение:
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
Middleware отвечает за:
Без middleware RTK Query не сможет:
Middleware — центральный runtime engine библиотеки.
Интеграция выполняется через reducer и middleware.
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})
RTK Query автоматически генерирует hooks.
Из:
getUsers: builder.query(...)
получается:
useGetUsersQuery()
Они скрывают:
UI получает декларативный API:
const { data } = useGetUsersQuery()
RTK Query поддерживает lazy architecture.
const [trigger, result] = useLazyGetUsersQuery()
Запрос выполняется вручную:
trigger()
RTK Query поддерживает потоковое обновление через lifecycle API.
onCacheEntryAddedПозволяет подключать WebSocket:
getMessages: builder.query({
query: () => '/messages',
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded }
) {
await cacheDataLoaded
const socket = new WebSocket('ws://localhost')
socket.onmess age = (event) => {
const message = JSON.parse(event.data)
updateCachedData((draft) => {
draft.push(message)
})
}
}
})
RTK Query поддерживает optimistic UI.
onQueryStartedПример:
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(
{ id, ...patch },
{ dispatch, queryFulfilled }
) {
const patchResult = dispatch(
api.util.updateQueryData(
'getUser',
id,
(draft) => {
Object.assign(draft, patch)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
})
RTK Query генерирует внутренние Redux actions.
Примеры:
api/executeQuery/pending
api/executeQuery/fulfilled
api/executeQuery/rejected
Аргументы query сериализуются:
useGetUserQuery({
id: 5
})
RTK Query создает deterministic cache key.
Это критически важно для deduplication.
RTK Query генерирует selectors.
Пример:
api.endpoints.getUsers.select()
Можно получать cache data напрямую из store.
RTK Query поддерживает динамическое расширение API.
injectEndpointsПример:
const extendedApi = api.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
})
})
})
Позволяет:
Крупные приложения обычно делят API по доменам.
Пример:
src/
services/
auth/
users/
posts/
comments/
Часто используется структура:
features/
users/
api/
components/
hooks/
posts/
api/
components/
RTK Query способствует созданию единого data layer.
Вместо:
fetch()
axios()
custom hooks
useEffect
используется единая архитектура.
RTK Query поддерживает server-side rendering.
Особенно важно для:
Server cache может передаваться клиенту:
extractRehydrationInfo
Это уменьшает повторные запросы после hydration.
Ошибки являются частью query state.
const {
error,
isError
} = useGetUsersQuery()
RTK Query унифицирует ошибки:
{
status,
data
}
Это упрощает глобальную обработку.
RTK Query поддерживает retry wrapper.
Пример:
import { retry } from '@reduxjs/toolkit/query'
const staggeredBaseQuery = retry(
fetchBaseQuery({ baseUrl: '/' }),
{
maxRetries: 5
}
)
RTK Query оптимизирован для минимизации re-render.
Если данные не изменились:
data === previousData
ссылка сохраняется.
Это уменьшает количество рендеров.
Компоненты подписываются только на нужные query.
Изменение одного cache entry не обновляет весь store.
selectFromResultПозволяет подписаться только на часть данных.
const { user } = useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.find((u) => u.id === 5)
})
})
Вся серверная логика находится в одном слое.
RTK Query автоматически реализует:
Архитектура декларативна:
query
mutation
tags
Подходит для:
RTK Query не строит entity graph автоматически.
RTK Query тесно связан с Redux Toolkit.
onQueryStarted onCacheEntryAdded
могут существенно усложнять архитектуру.
Для простого CRUD иногда достаточно:
fetch + useEffect
Однако в средних и крупных системах RTK Query значительно уменьшает инфраструктурный код и упрощает управление серверным состоянием.