По мере роста приложения единый API-слайс начинает превращаться в перегруженную структуру, содержащую десятки или сотни endpoints. Это приводит к нескольким проблемам:
RTK Query предоставляет механизм масштабирования через разделение API-слайсов и динамическое расширение endpoints.
Наиболее распространённый подход — создание одного базового API и
расширение его через injectEndpoints.
Базовая конфигурация:
// shared/api/baseApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
tagTypes: ['User', 'Post', 'Comment'],
endpoints: () => ({})
})
Особенности такого подхода:
Каждый модуль приложения может расширять базовый API собственными endpoints.
// entities/user/api/userApi.js
import { baseApi } from '@/shared/api/baseApi'
export const userApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['User']
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [
{ type: 'User', id }
]
})
})
})
// entities/post/api/postApi.js
import { baseApi } from '@/shared/api/baseApi'
export const postApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
createPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
}),
invalidatesTags: ['Post']
})
})
})
После расширения API автоматически создаются hooks.
export const {
useGetUsersQuery,
useGetUserByIdQuery
} = userApi
export const {
useGetPostsQuery,
useCreatePostMutation
} = postApi
Каждый модуль экспортирует только собственные hooks, что повышает инкапсуляцию.
На практике endpoints обычно группируются по бизнес-доменам.
Типичная структура:
src/
├─ app/
├─ shared/
│ └─ api/
│ └─ baseApi.js
├─ entities/
│ ├─ user/
│ │ └─ api/
│ │ └─ userApi.js
│ ├─ post/
│ │ └─ api/
│ │ └─ postApi.js
│ └─ comment/
│ └─ api/
│ └─ commentApi.js
Такой подход особенно хорошо сочетается с:
RTK Query допускает создание нескольких API-слайсов:
export const userApi = createApi({...})
export const postApi = createApi({...})
Однако такой подход создаёт ряд проблем.
reducer: {
[userApi.reducerPath]: userApi.reducer,
[postApi.reducerPath]: postApi.reducer
}
Количество reducers начинает быстро расти.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
userApi.middleware,
postApi.middleware
)
Каждый middleware добавляет дополнительную нагрузку.
Разные API-слайсы:
fetchBaseQuery({
baseUrl: '/api'
})
Такая конфигурация начинает повторяться в каждом API.
Несколько API-слайсов оправданы в следующих случаях:
baseUrl: '/auth-api'
baseUrl: '/payments-api'
Например:
Иногда части приложения требуют:
В микрофронтенд-архитектуре каждый модуль может иметь собственный API-слайс.
injectEndpoints поддерживает переопределение.
const extendedApi = baseApi.injectEndpoints({
overrideExisting: true,
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/v2/users'
})
})
})
Это полезно:
Одно из ключевых преимуществ разделения API — возможность lazy loading.
const UserPage = lazy(() => import('./UserPage'))
Внутри страницы:
import '@/entities/user/api/userApi'
Endpoints регистрируются только после загрузки модуля.
RTK Query поддерживает полноценный code splitting.
// baseApi.js
export const baseApi = createApi({
reducerPath: 'api',
baseQuery,
endpoints: () => ({})
})
// orderApi.js
export const orderApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getOrders: builder.query({
query: () => '/orders'
})
})
})
Chunk с orderApi будет загружен только при
необходимости.
Разделение API-слайсов помогает:
Особенно заметно в крупных enterprise-приложениях.
Все теги желательно регистрировать в базовом API.
tagTypes: [
'User',
'Post',
'Comment',
'Order'
]
Если тег отсутствует в tagTypes, RTK Query выдаст
предупреждение.
В больших приложениях удобно использовать константы.
// shared/api/tags.js
export const TAGS = {
USER: 'User',
POST: 'Post',
COMMENT: 'Comment'
}
Использование:
providesTags: [TAGS.USER]
Это снижает риск опечаток.
Иногда требуется несколько уровней baseQuery.
const rawBaseQuery = fetchBaseQuery({
baseUrl: '/api'
})
const baseQueryWithReauth = async (
args,
api,
extraOptions
) => {
let result = await rawBaseQuery(
args,
api,
extraOptions
)
if (result.error?.status === 401) {
// refresh token
}
return result
}
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithReauth,
endpoints: () => ({})
})
Все модули автоматически получают общую логику авторизации.
Хорошая практика — скрывать внутреннюю структуру endpoints.
export const api = baseApi.injectEndpoints(...)
const api = baseApi.injectEndpoints(...)
export const {
useGetUsersQuery
} = api
Модуль экспортирует только публичный контракт.
Неправильная организация API часто приводит к циклическим зависимостям.
userApi -> authApi
authApi -> userApi
Плохо:
import { userApi } from '../user/userApi'
внутри:
authApi.js
Вместо прямого вызова:
invalidatesTags: ['User']
shared/api/
shared/lib/
shared/config/
RTK Query хорошо работает с многослойной архитектурой.
baseApi
userApi
postApi
updateProfileApi
checkoutApi
Обычно API здесь не размещается, но возможно подключение feature-модулей.
Модульность значительно упрощает тестирование.
import { userApi } from './userApi'
Можно мокать только нужный модуль.
jest.mock('./userApi')
При Server-Side Rendering разделение API помогает:
Даже разделённые endpoints используют единый store.
dispatch(
baseApi.util.prefetch(
'getUsers',
undefined,
{ force: true }
)
)
Все endpoints остаются частью одного API-контейнера.
RTK Query не удаляет injected endpoints автоматически.
После инъекции endpoint остаётся зарегистрированным до перезагрузки приложения.
Это важно учитывать:
TypeScript корректно объединяет типы injected endpoints.
export const userApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
Типы автоматически расширяются.
Во время HMR endpoints могут инжектироваться повторно.
Для предотвращения предупреждений используется:
overrideExisting: false
или:
overrideExisting: 'throw'
Для крупных проектов удобно использовать barrel-файлы.
// entities/user/index.js
export * from './api/userApi'
Иногда API разделяют не по сущностям, а по операциям.
userQueriesApi.js
userMutationsApi.js
Подход встречается редко, но полезен:
Грамотное разделение API-слайсов:
В большинстве современных приложений оптимальной стратегией считается:
createApi;injectEndpoints;