RTK Query поддерживает отложенную (lazy) загрузку API-слайсов как способ уменьшить начальный размер бандла, ускорить старт приложения и загружать API-логики только в момент реальной необходимости. Этот подход особенно важен в крупных приложениях, где набор эндпоинтов велик, но используется фрагментарно в зависимости от маршрута, роли пользователя или состояния интерфейса.
Lazy-подход в RTK Query реализуется через динамическое создание API-слайсов и подключение редьюсеров и middleware в runtime. Основная идея заключается не в «ленивом запросе», а в ленивой регистрации самого API слоя в Redux store.
RTK Query строится вокруг объекта createApi, который
генерирует:
В стандартной схеме API объявляется статически:
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
});
В lazy-сценарии такой подход заменяется динамическим созданием API-модулей или расширением базового API через code splitting.
Основные причины внедрения lazy API-слайсов:
RTK Query генерирует значительное количество кода:
Если приложение содержит десятки API-модулей, их статическая загрузка увеличивает время первого рендера.
Типичная архитектура:
Каждый домен может быть изолирован и загружаться отдельно.
Lazy-загрузка позволяет:
Существует несколько архитектурных подходов.
Основной встроенный механизм RTK Query —
injectEndpoints.
Он позволяет расширять базовый API после его создания.
export const baseApi = createApi({
reducerPath: 'baseApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: () => ({})
});
const extendedApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
});
injectEndpoints:
Наиболее гибкий способ — сочетание injectEndpoints и
import().
export const loadUsersApi = async () => {
const module = await import('./usersApi');
return module.usersApi;
};
usersApi.js:
import { baseApi } from './baseApi';
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
});
const onOpenUs ers = async () => {
const { usersApi } = await import('./api/usersApi');
const result = usersApi.endpoints.getUsers.useQuery();
};
Однако хуки нельзя вызывать после условного import напрямую. Поэтому чаще используется стратегия предварительной регистрации.
RTK Query требует подключения middleware:
import { configureStore } from '@reduxjs/toolkit';
import { baseApi } from './baseApi';
export const store = configureStore({
reducer: {
[baseApi.reducerPath]: baseApi.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware)
});
Lazy API требует, чтобы store поддерживал динамическое расширение reducer.
store.asyncReducers = {};
store.injectReducer = (key, reducer) => {
store.asyncReducers[key] = reducer;
store.replaceReducer(
combineReducers({
...store.asyncReducers
})
);
};
import { usersApi } from './usersApi';
store.injectReducer(usersApi.reducerPath, usersApi.reducer);
store.dispatch(usersApi.util.resetApiState());
Наиболее масштабируемый подход — разделение API по feature-модулям.
/api
baseApi.js
/features
users
usersApi.js
usersSlice.js
auth
authApi.js
import { baseApi } from '../. ./api/baseApi';
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
}),
getUserById: builder.query({
query: (id) => `/users/${id}`
})
})
});
export const loadUsersFeature = async () => {
const module = await import('./usersApi');
return module.usersApi;
};
RTK Query часто интегрируется с маршрутизацией.
const UsersPage = React.lazy(() => import('./UsersPage'));
Внутри страницы:
import { usersApi } from './usersApi';
export const UsersPage = () => {
const { data } = usersApi.useGetUsersQuery();
return <div>{JSON.stringify(data)}</div>;
};
Проблема здесь в том, что API может быть загружен вместе с компонентом, но middleware и reducer уже должны быть зарегистрированы заранее.
Часто используется паттерн bootstrap:
export const initApiModule = (api) => {
store.injectReducer(api.reducerPath, api.reducer);
};
const module = await import('./features/users/usersApi');
initApiModule(module.usersApi);
RTK Query использует единый cache key space внутри одного
reducerPath.
При lazy загрузке важно:
Создание нескольких createApi с одинаковыми endpoints
приводит к:
Если компонент уже использует useQuery, но reducer не
подключён:
Если один и тот же endpoint инжектится несколько раз:
Наиболее стабильная архитектура строится вокруг одного base API:
createApiinjectEndpointsexport const loadFeature = async (featureName) => {
switch (featureName) {
case 'users':
return import('./features/users/usersApi');
case 'auth':
return import('./features/auth/authApi');
}
};
const registry = new Map();
export const registerApi = (api) => {
if (!registry.has(api.reducerPath)) {
registry.set(api.reducerPath, api);
store.injectReducer(api.reducerPath, api.reducer);
}
};
Lazy API слой в RTK Query фактически представляет собой комбинацию:
createApi как базовый контрактinjectEndpoints как механизм расширенияdynamic import как механизм загрузкиstore.replaceReducer как механизм интеграции в
ReduxТакой подход позволяет масштабировать API слой без увеличения стартового бандла и без потери консистентности состояния кеша.