RTK Query — библиотека для получения, кэширования, синхронизации и управления серверными данными в приложениях на Javascript. Библиотека входит в экосистему Redux Toolkit и решает типичные проблемы клиент-серверного взаимодействия:
RTK Query значительно сокращает объём шаблонного кода по сравнению с
ручным использованием fetch, axios или
классического Redux.
Для работы RTK Query требуется современное Javascript-окружение.
Минимальный набор:
Наиболее распространённый стек:
Node.js
React
Redux Toolkit
RTK Query
TypeScript (опционально)
Проверка установленного Node.js:
node -v
Проверка npm:
npm -v
Современный способ инициализации React-приложения — использование Vite.
Создание проекта:
npm create vite@latest rtk-query-app
Выбор параметров:
Framework: React
Variant: JavaScript
Переход в каталог проекта:
cd rtk-query-app
Установка зависимостей:
npm install
Запуск dev-сервера:
npm run dev
RTK Query встроен в Redux Toolkit, поэтому отдельная установка основной библиотеки RTK Query не требуется.
Установка:
npm install @reduxjs/toolkit react-redux
После установки становятся доступны:
Базовая структура приложения:
src/
├── app/
│ └── store.js
├── services/
│ └── api.js
├── features/
├── components/
├── pages/
├── hooks/
├── App.jsx
└── main.jsx
app/Содержит глобальную конфигурацию Redux Store.
services/Содержит API-сервисы RTK Query.
features/Модули бизнес-логики.
components/Переиспользуемые React-компоненты.
pages/Компоненты страниц.
hooks/Кастомные React-hooks.
Создание файла:
src/app/store.js
Базовая конфигурация:
import { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
reducerПодключает reducer RTK Query в глобальное состояние Redux.
[api.reducerPath]: api.reducer
По умолчанию reducerPath:
'api'
Итоговое состояние:
state.api
middlewareRTK Query использует middleware для:
Подключение middleware обязательно:
getDefaultMiddleware().concat(api.middleware)
Без middleware библиотека работать корректно не будет.
Создание файла:
src/services/api.js
Базовый пример:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com/',
}),
endpoints: () => ({}),
})
createApiГлавная функция RTK Query для создания API-слоя.
Она автоматически генерирует:
reducerPathОпределяет имя раздела в Redux Store.
reducerPath: 'api'
Состояние будет храниться:
state.api
baseQueryБазовый механизм выполнения запросов.
Наиболее популярный вариант:
fetchBaseQuery
Это обёртка над стандартным fetch.
baseUrlБазовый URL для всех запросов:
baseUrl: 'https://jsonplaceholder.typicode.com/'
Теперь endpoint:
/posts
будет автоматически преобразован в:
https://jsonplaceholder.typicode.com/posts
endpointsОписание всех API-endpoints приложения.
Пока объект пуст:
endpoints: () => ({})
Endpoints будут добавляться позже.
Файл:
src/main.jsx
Настройка:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
Компонент Provider из React Redux делает Redux Store
доступным всему React-приложению.
Без Provider:
После создания endpoints RTK Query автоматически генерирует hooks.
Пример:
export const {
useGetPostsQuery,
} = api
Эти hooks используются внутри React-компонентов.
Расширение api.js:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com/',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
})
export const {
useGetPostsQuery,
} = api
builder.queryСоздаёт GET-запрос.
builder.query({
query: () => 'posts',
})
Запрос:
GET /posts
queryВозвращает URL endpoint.
query: () => 'posts'
RTK Query автоматически объединяет:
baseUrl + query()
Пример компонента:
import { useGetPostsQuery } from '../services/api'
export default function Posts() {
const {
data,
error,
isLoading,
} = useGetPostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error</div>
}
return (
<ul>
{data.map((post) => (
<li key={post.id}>
{post.title}
</li>
))}
</ul>
)
}
RTK Query автоматически управляет состояниями запроса.
dataПолученные данные.
errorОшибка запроса.
isLoadingПервая загрузка данных.
isFetchingЛюбой активный запрос.
isSuccessУспешное выполнение.
isErrorОшибка выполнения.
Redux Toolkit автоматически поддерживает Redux DevTools.
Дополнительная настройка обычно не требуется.
В DevTools можно анализировать:
Для крупных проектов полезно настроить абсолютные импорты.
Файл:
vite.config.js
Пример:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Вместо:
import { store } from '../. ./. ./app/store'
Можно использовать:
import { store } from '@/app/store'
В больших проектах API рекомендуется декомпозировать.
Пример структуры:
services/
├── api/
│ ├── baseApi.js
│ ├── postsApi.js
│ ├── usersApi.js
│ └── commentsApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com/',
}),
endpoints: () => ({}),
})
import { baseApi } from './baseApi'
export const postsApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
})
API можно разделять по доменам.
Endpoints могут загружаться динамически.
Упрощается сопровождение проекта.
Каждый модуль содержит собственные endpoints.
RTK Query предоставляет множество возможностей конфигурации.
Пример:
fetchBaseQuery({
baseUrl: 'https://api.example.com/',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
},
})
Позволяет динамически изменять headers перед запросом.
Чаще всего используется для:
fetchBaseQuery не поддерживает timeout напрямую.
Создание обёртки:
const baseQuery = fetchBaseQuery({
baseUrl: 'https://api.example.com/',
})
const baseQueryWithTimeout = async (
args,
api,
extraOptions
) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
return Promise.race([
baseQuery(args, api, extraOptions),
timeout,
])
}
.envVITE_API_URL=https://api.example.com
fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_URL,
})
Установка:
npm install -D eslint
Инициализация:
npx eslint --init
Для React и RTK Query особенно важны:
{
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
Установка:
npm install -D prettier
Создание файла:
.prettierrc
Пример:
{
"semi": false,
"singleQuote": true
}
RTK Query особенно хорошо интегрирован с TypeScript.
Установка:
npm install -D typescript
Создание TypeScript-проекта:
npm create vite@latest
Выбор:
React + TypeScript
import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
getPosts: builder.query<Post[], void>({
query: () => 'posts',
})
type Post = {
id: number
title: string
body: string
}
const { data } = useGetPostsQuery()
Тип data будет автоматически определён:
Post[] | undefined
Типичная структура:
src/
├── app/
├── services/
├── shared/
├── entities/
├── features/
├── widgets/
└── pages/
Такой подход хорошо сочетается с:
Ошибка:
middleware: []
Правильно:
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
Ошибка:
reducer: {}
Правильно:
reducer: {
[api.reducerPath]: api.reducer,
}
Без Provider hooks работать не будут.
Нужно импортировать:
@reduxjs/toolkit/query/react
А не:
@reduxjs/toolkit/query
если используются React hooks.
Корректно настроенный проект должен обеспечивать:
store.jsimport { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
api.jsimport { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com/',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
})
export const {
useGetPostsQuery,
} = api
main.jsximport React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
App.jsximport { useGetPostsQuery } from './services/api'
export default function App() {
const {
data,
isLoading,
} = useGetPostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{data.map((post) => (
<div key={post.id}>
{post.title}
</div>
))}
</div>
)
}