RTK Query редко используется полностью изолированно. В большинстве приложений библиотека становится частью более крупной архитектуры, включающей:
Главная особенность RTK Query заключается в том, что он отвечает исключительно за слой работы с удалёнными данными. Это позволяет комбинировать его практически с любыми решениями без конфликтов архитектуры.
RTK Query является частью экосистемы Redux Toolkit, поэтому интеграция с обычными slice выполняется естественным образом.
Типичная схема:
| Задача | Инструмент |
|---|---|
| Серверные данные | RTK Query |
| UI state | Redux slice |
| Модальные окна | Redux slice |
| Фильтры интерфейса | Redux slice |
| Кэш API | RTK Query |
| Локальные формы | Redux slice / form library |
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
import authReducer from './features/auth/authSlice'
import uiReducer from './features/ui/uiSlice'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authReducer,
ui: uiReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})
RTK Query не заменяет Redux store, а становится его специализированной частью.
RTK Query предоставляет доступ к Redux dispatch через lifecycle API.
getProfile: builder.query({
query: () => '/profile',
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled
dispatch(setProfileLoaded(true))
dispatch(setUser(data))
} catch (error) {
dispatch(setProfileError(true))
}
}
})
Такой подход позволяет:
RTK Query хорошо интегрируется с маршрутизацией.
import { api } from './services/api'
const prefetchPost = api.util.prefetch('getPost', 15, {
force: true
})
<Link
to="/posts/15"
onMouseEn ter={() => dispatch(prefetchPost)}
>
Открыть пост
</Link>
Данные начинают загружаться ещё до перехода между страницами.
import { useParams } from 'react-router-dom'
function PostPage() {
const { id } = useParams()
const { data, isLoading } = useGetPostQuery(id)
if (isLoading) {
return <div>Loading...</div>
}
return <div>{data.title}</div>
}
После мутаций часто требуется обновление данных при переходах.
updatePost: builder.mutation({
query: ({ id, ...body }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: ['Posts']
})
После возврата на список постов RTK Query автоматически инициирует повторную загрузку.
RTK Query часто используется вместе с:
function ProfileForm() {
const { data } = useGetProfileQuery()
const {
register,
handleSubmit,
reset
} = useForm()
useEffect(() => {
if (data) {
reset(data)
}
}, [data])
const [updateProfile] = useUpdateProfileMutation()
const onSub mit = async (values) => {
await updateProfile(values)
}
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register('name')} />
<input {...register('email')} />
<button>Save</button>
</form>
)
}
const onSub mit = async (values) => {
try {
await updateProfile(values).unwrap()
} catch (error) {
setError('email', {
type: 'server',
message: error.data.message
})
}
}
RTK Query удобно комбинируется с validation layer формы.
RTK Query по умолчанию использует fetchBaseQuery, однако возможно подключение Axios.
import axios from 'axios'
const axiosBaseQuery =
({ baseUrl } = { baseUrl: '' }) =>
async ({ url, method, data, params, headers }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data,
params,
headers
})
return { data: result.data }
} catch (axiosError) {
const err = axiosError
return {
error: {
status: err.response?.status,
data: err.response?.data
}
}
}
}
export const api = createApi({
reducerPath: 'api',
baseQuery: axiosBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => ({
url: '/users',
method: 'GET'
})
})
})
})
Чаще всего Axios подключается для:
Во многих случаях fetchBaseQuery оказывается проще и легче.
RTK Query способен работать не только с REST API.
import { request } from 'graphql-request'
const graphqlBaseQuery =
({ baseUrl }) =>
async ({ body }) => {
try {
const result = await request(
baseUrl,
body
)
return { data: result }
} catch (error) {
return {
error: {
status: error.response.status,
data: error
}
}
}
}
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: '/graphql'
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
body: `
query {
posts {
id
title
}
}
`
})
})
})
})
При использовании GraphQL:
Тем не менее RTK Query остаётся удобным orchestration layer.
RTK Query не заменяет WebSocket, но отлично работает вместе с ним.
getMessages: builder.query({
query: () => '/messages',
async onCacheEntryAdded(
arg,
{
updateCachedData,
cacheDataLoaded,
cacheEntryRemoved
}
) {
const socket = new WebSocket('ws://localhost:3000')
try {
await cacheDataLoaded
socket.onmess age = (event) => {
const message = JSON.parse(event.data)
updateCachedData((draft) => {
draft.push(message)
})
}
} catch {}
await cacheEntryRemoved
socket.close()
}
})
Популярная архитектура:
| Технология | Роль |
|---|---|
| RTK Query | Первичная загрузка |
| WebSocket | Real-time обновления |
| RTK Query cache | Хранилище |
| React UI | Отображение |
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000')
socket.on('newPost', (post) => {
store.dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.unshift(post)
}
)
)
})
RTK Query превращается в единый источник серверного состояния независимо от транспорта.
RTK Query поддерживает server-side rendering.
Особенно часто используется совместно с:
store.dispatch(
api.endpoints.getPosts.initiate()
)
await Promise.all(
store.dispatch(
api.util.getRunningQueriesThunk()
)
)
import { HYDRATE } from 'next-redux-wrapper'
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
}
RTK Query может работать с persisted state, однако требует осторожности.
Наиболее безопасный вариант:
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth']
}
Кэш RTK Query обычно не сохраняется.
Проблемы:
Иногда persist используется для:
В таких случаях необходима ручная стратегия инвалидирования.
RTK Query хранит данные в query cache, но иногда требуется нормализованная структура.
import {
createEntityAdapter
} from '@reduxjs/toolkit'
const usersAdapter = createEntityAdapter()
const initialState =
usersAdapter.getInitialState()
getUsers: builder.query({
query: () => '/users',
transformResponse(response) {
return usersAdapter.setAll(
initialState,
response
)
}
})
Нормализация позволяет:
RTK Query уже содержит эффективный query cache.
Избыточная нормализация часто усложняет код без реальной пользы.
RTK Query хорошо сочетается с runtime validation.
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
getUser: builder.query({
query: (id) => `/users/${id}`,
transformResponse(response) {
return UserSchema.parse(response)
}
})
Валидация позволяет:
Mock Service Worker стал практически стандартом для тестирования RTK Query.
import { rest } from 'msw'
export const handlers = [
rest.get('/api/posts', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
title: 'Post'
}
])
)
})
]
render(<PostsPage />)
expect(
await screen.findByText('Post')
).toBeInTheDocument()
MSW:
RTK Query постепенно развивается в сторону поддержки Suspense.
<Suspense fallback={<Loader />}>
<PostsPage />
</Suspense>
При использовании Suspense:
RTK Query ошибки удобно комбинируются с React Error Boundary.
class ErrorBoundary extends React.Component {
state = {
hasError: false
}
static getDerivedStateFromError() {
return {
hasError: true
}
}
render() {
if (this.state.hasError) {
return <div>Ошибка</div>
}
return this.props.children
}
}
if (error) {
throw error
}
Ошибка передаётся в Error Boundary автоматически.
RTK Query удобно интегрируется с системами мониторинга.
async onQueryStarted(arg, api) {
try {
await api.queryFulfilled
} catch (error) {
Sentry.captureException(error)
}
}
RTK Query хорошо вписывается в FSD-архитектуру.
src/
├─ app/
├─ pages/
├─ widgets/
├─ features/
├─ entities/
│ ├─ user/
│ │ ├─ api/
│ │ ├─ model/
│ │ ├─ ui/
export const userApi = createApi({
reducerPath: 'userApi',
endpoints: () => ({})
})
export const extendedApi =
baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
Такой подход особенно удобен для code splitting.
RTK Query хорошо подходит для monorepo-архитектур.
packages/
├─ api/
├─ web/
├─ admin/
├─ mobile/
export const api = createApi({
baseQuery,
endpoints: () => ({})
})
export const usersApi =
api.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
RTK Query может использоваться внутри независимых frontend-модулей.
createApi({
reducerPath: 'billingApi'
})
createApi({
reducerPath: 'profileApi'
})
Каждый microfrontend получает собственный namespace.
При большом количестве API slice важно:
RTK Query способен работать в desktop-приложениях.
В Electron RTK Query используется для:
RTK Query поддерживает React Native практически без изменений.
На мобильных устройствах важно учитывать:
setupListeners(store.dispatch)
useGetPostsQuery(undefined, {
refetchOnReconnect: true
})
После восстановления сети запросы автоматически обновляются.
RTK Query может использоваться даже без полного перехода на Redux.
const useUiStore = create((set) => ({
modalOpen: false,
setModalOpen: (value) =>
set({
modalOpen: value
})
}))
const { data } = useGetUserQuery()
Zustand управляет UI state, RTK Query — серверными данными.
const filterAtom = atom('')
const filter = useAtomValue(filterAtom)
const { data } = useGetPostsQuery(filter)
const authState = atom({
key: 'auth',
default: null
})
RTK Query остаётся отдельным серверным слоем.
На практике часто используются смешанные схемы.
| Слой | Инструмент |
|---|---|
| Серверный state | RTK Query |
| UI state | Zustand |
| Формы | React Hook Form |
| Навигация | React Router |
| Валидация | Zod |
| WebSocket | Socket.IO |
| Тестирование | MSW |
RTK Query:
Именно эта гибкость сделала RTK Query универсальным инструментом для современных frontend-приложений различного масштаба и архитектурной сложности.