Функция createApi является центральным элементом
библиотеки RTK Query. Именно она формирует API-слой приложения,
генерирует endpoints, создает middleware, reducer, React hooks и
управляет системой кэширования.
Внутри RTK Query практически вся архитектура строится вокруг одного
или нескольких экземпляров API, созданных через
createApi.
Базовый пример:
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'
})
})
})
После выполнения createApi создается объект API,
содержащий:
api.reducer
api.middleware
api.endpoints
api.util
api.injectEndpoints
При использовании React автоматически генерируются hooks:
api.useGetPostsQuery
createApiФункция принимает конфигурационный объект:
createApi({
reducerPath,
baseQuery,
tagTypes,
endpoints,
keepUnusedDataFor,
refetchOnFocus,
refetchOnReconnect,
refetchOnMountOrArgChange,
extractRehydrationInfo,
serializeQueryArgs,
invalidationBehavior
})
Каждое поле влияет на поведение всей системы API.
reducerPathreducerPath определяет имя раздела store, в котором RTK
Query хранит свое состояние.
Пример:
reducerPath: 'api'
В Redux Store появится раздел:
state.api
Внутри будут храниться:
{
api: {
queries: {},
mutations: {},
provided: {},
subscriptions: {},
config: {}
}
}
Можно создавать несколько API:
export const usersApi = createApi({
reducerPath: 'usersApi',
...
})
export const postsApi = createApi({
reducerPath: 'postsApi',
...
})
Тогда store будет содержать:
{
usersApi: {},
postsApi: {}
}
Каждый reducerPath обязан быть уникальным.
Ошибка:
createApi({
reducerPath: 'api'
})
createApi({
reducerPath: 'api'
})
Это приведет к конфликту reducers и middleware.
baseQuerybaseQuery — базовый механизм выполнения
HTTP-запросов.
RTK Query вызывает его для каждого endpoint.
fetchBaseQueryНаиболее распространенный вариант:
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'
baseQuery: fetchBaseQuery({
baseUrl: '/api'
})
Это обертка над fetch.
fetchBaseQueryПри запросе:
query: () => 'users'
будет выполнено:
fetch('/api/users')
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
}
})
prepareHeadersИспользуется для:
prepareHeaders: (headers, { getState }) => {
const lang = getState().settings.language
headers.set('Accept-Language', lang)
return headers
}
baseQueryВместо fetchBaseQuery можно написать собственную
функцию.
Пример:
const customBaseQuery = async (args, api, extraOptions) => {
try {
const response = await axios(args)
return {
data: response.data
}
} catch (error) {
return {
error: {
status: error.response?.status,
data: error.response?.data
}
}
}
}
baseQueryconst baseQuery = async (
args,
api,
extraOptions
) => {}
argsСодержит параметры запроса:
{
url,
method,
body,
params
}
apiСодержит:
api.dispatch
api.getState
api.signal
api.abort
api.endpoint
api.type
signalИспользуется для отмены запросов:
const response = await fetch(url, {
signal: api.signal
})
extraOptionsПозволяет передавать дополнительные настройки endpoint.
tagTypestagTypes описывает список тегов, используемых системой
инвалидации кэша.
Пример:
tagTypes: ['Post', 'User']
RTK Query связывает:
Через:
providesTags
invalidatesTags
getPosts: builder.query({
query: () => 'posts',
providesTags: ['Post']
})
Mutation:
addPost: builder.mutation({
query: (body) => ({
url: 'posts',
method: 'POST',
body
}),
invalidatesTags: ['Post']
})
После mutation RTK Query автоматически перезапросит
getPosts.
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: 'Post',
id
})),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
invalidatesTags: (result, error, id) => [
{ type: 'Post', id }
]
endpointsendpoints описывает все API endpoints.
Пример:
endpoints: (builder) => ({
getPosts: builder.query({}),
addPost: builder.mutation({})
})
builderBuilder предоставляет:
builder.query()
builder.mutation()
builder.queryИспользуется для GET-запросов и операций чтения.
Пример:
getUsers: builder.query({
query: () => 'users'
})
builder.mutationИспользуется для:
Пример:
updateUser: builder.mutation({
query: ({ id, ...body }) => ({
url: `users/${id}`,
method: 'PATCH',
body
})
})
keepUnusedDataForОпределяет время хранения кэша после отписки компонентов.
keepUnusedDataFor: 60
Данные будут храниться 60 секунд.
Когда последний компонент отписывается:
const { data } = useGetPostsQuery()
RTK Query запускает таймер удаления.
Если компонент снова подпишется:
useGetPostsQuery()
до удаления кэша, повторного запроса не произойдет.
refetchOnFocusАвтоматический refetch при возвращении вкладки в фокус.
refetchOnFocus: true
При переключении вкладок:
refetchOnReconnectПовторный запрос после восстановления интернета.
refetchOnReconnect: true
refetchOnMountOrArgChangeУправляет refetch при:
refetchOnMountOrArgChange: true
Всегда выполнять повторный запрос.
refetchOnMountOrArgChange: 30
Повторный запрос только если кэш старше 30 секунд.
extractRehydrationInfoИспользуется для SSR и hydration.
Особенно важно в:
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
}
serializeQueryArgsУправляет генерацией cache key.
RTK Query сериализует аргументы автоматически:
useGetUserQuery(5)
Ключ:
getUser(5)
serializeQueryArgs: ({
endpointName,
queryArgs
}) => {
return `${endpointName}-${queryArgs.id}`
}
Полезно для:
invalidationBehaviorОпределяет момент инвалидации тегов.
delayedПоведение по умолчанию.
invalidationBehavior: 'delayed'
Инвалидация откладывается до завершения всех запросов.
immediatelyinvalidationBehavior: 'immediately'
Инвалидация выполняется сразу.
RTK Query автоматически создает React hooks.
const {
data,
error,
isLoading
} = useGetPostsQuery()
const [
addPost,
result
] = useAddPostMutation()
const [
trigger,
result
] = useLazyGetPostsQuery()
api.endpointsПосле создания API появляется объект endpoints:
api.endpoints.getPosts
api.endpoints.getPosts.initiate()
api.endpoints.getPosts.select()
initiatedispatch(
api.endpoints.getPosts.initiate()
)
Используется:
createApi создает middleware:
api.middleware
Middleware управляет:
createApi генерирует reducer:
api.reducer
Подключение:
configureStore({
reducer: {
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})
RTK Query проходит несколько стадий:
Каждый запрос получает уникальный ключ.
useGetUserQuery(1)
Ключ:
getUser(1)
useGetUserQuery(1)
useGetUserQuery(1)
Используют один cache entry.
RTK Query автоматически объединяет одинаковые запросы.
Три компонента:
useGetPostsQuery()
Выполнят только один HTTP-запрос.
createApi поддерживает polling.
useGetPostsQuery(undefined, {
pollingInterval: 5000
})
Запрос каждые 5 секунд.
RTK Query поддерживает WebSocket и SSE через lifecycle API.
async onCacheEntryAdded(
arg,
{
updateCachedData,
cacheDataLoaded,
cacheEntryRemoved
}
) {
const ws = new WebSocket('ws://localhost')
try {
await cacheDataLoaded
ws.onmess age = (event) => {
const data = JSON.parse(event.data)
updateCachedData((draft) => {
draft.push(data)
})
}
await cacheEntryRemoved
ws.close()
} catch {
ws.close()
}
}
RTK Query поддерживает динамическое расширение API.
injectEndpointsconst extendedApi = api.injectEndpoints({
endpoints: (builder) => ({
getComments: builder.query({
query: () => 'comments'
})
})
})
Позволяет:
api.utilcreateApi генерирует набор utilities.
invalidateTagsdispatch(
api.util.invalidateTags(['Post'])
)
resetApiStatedispatch(api.util.resetApiState())
Полностью очищает кэш.
updateQueryDatadispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
draft.push(newPost)
}
)
)
RTK Query поддерживает optimistic updates.
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(
arg,
{ dispatch, queryFulfilled }
) {
const patchResult = dispatch(
api.util.updateQueryData(
'getPosts',
undefined,
(draft) => {
const post = draft.find(
(p) => p.id === arg.id
)
Object.assign(post, arg)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
})
createApiОшибка:
middleware: []
Без:
api.middleware
RTK Query работать не будет.
Ошибка:
reducer: {}
Без:
[api.reducerPath]: api.reducer
кэширование и state management не работают.
reducerPathВызывает конфликты store.
tagTypesИнвалидация может работать некорректно.
Крупные приложения обычно разделяют API:
/api
baseApi.js
usersApi.js
postsApi.js
commentsApi.js
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: () => ({})
})
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users'
})
})
})
createApi проектировалась с учетом минимизации:
RTK Query использует:
Несколько createApi имеют смысл при:
Один createApi предпочтительнее при:
createApiimport {
createApi,
fetchBaseQuery
} from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set(
'authorization',
`Bearer ${token}`
)
}
return headers
}
}),
tagTypes: ['Post', 'User'],
keepUnusedDataFor: 60,
refetchOnFocus: true,
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
providesTags: ['Post']
}),
getUser: builder.query({
query: (id) => `users/${id}`,
providesTags: (result, error, id) => [
{ type: 'User', id }
]
}),
addPost: builder.mutation({
query: (body) => ({
url: 'posts',
method: 'POST',
body
}),
invalidatesTags: ['Post']
}),
updateUser: builder.mutation({
query: ({ id, ...body }) => ({
url: `users/${id}`,
method: 'PATCH',
body
}),
invalidatesTags: (result, error, arg) => [
{
type: 'User',
id: arg.id
}
]
})
})
})