Lazy загрузка API слайсов

RTK Query поддерживает отложенную (lazy) загрузку API-слайсов как способ уменьшить начальный размер бандла, ускорить старт приложения и загружать API-логики только в момент реальной необходимости. Этот подход особенно важен в крупных приложениях, где набор эндпоинтов велик, но используется фрагментарно в зависимости от маршрута, роли пользователя или состояния интерфейса.

Lazy-подход в RTK Query реализуется через динамическое создание API-слайсов и подключение редьюсеров и middleware в runtime. Основная идея заключается не в «ленивом запросе», а в ленивой регистрации самого API слоя в Redux store.


RTK Query строится вокруг объекта createApi, который генерирует:

  • редьюсер
  • middleware
  • хуки для React
  • кеш и нормализацию запросов

В стандартной схеме API объявляется статически:

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users'
    })
  })
});

В lazy-сценарии такой подход заменяется динамическим созданием API-модулей или расширением базового API через code splitting.


Причины использования lazy загрузки

Основные причины внедрения lazy API-слайсов:

Сокращение начального бандла

RTK Query генерирует значительное количество кода:

  • endpoints
  • selectors
  • hooks
  • middleware logic

Если приложение содержит десятки API-модулей, их статическая загрузка увеличивает время первого рендера.

Разделение по доменным областям

Типичная архитектура:

  • auth API
  • users API
  • admin API
  • billing API
  • analytics API

Каждый домен может быть изолирован и загружаться отдельно.

Контроль над доступом

Lazy-загрузка позволяет:

  • не загружать admin API для обычных пользователей
  • подгружать функциональность по ролям
  • экономить память и сеть

Способы реализации lazy API в RTK Query

Существует несколько архитектурных подходов.


1. Динамическая регистрация API через injectEndpoints

Основной встроенный механизм RTK Query — injectEndpoints.

Он позволяет расширять базовый API после его создания.

Базовый API

export const baseApi = createApi({
  reducerPath: 'baseApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: () => ({})
});

Lazy расширение

const extendedApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users'
    })
  })
});

Особенность поведения

injectEndpoints:

  • не пересоздаёт store
  • добавляет новые endpoints в runtime
  • сохраняет общий cache
  • расширяет middleware поведение

Lazy загрузка через динамический import

Наиболее гибкий способ — сочетание 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 напрямую. Поэтому чаще используется стратегия предварительной регистрации.


2. Lazy регистрация через Redux middleware setup

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.


Динамическая подмена reducer

store.asyncReducers = {};

store.injectReducer = (key, reducer) => {
  store.asyncReducers[key] = reducer;
  store.replaceReducer(
    combineReducers({
      ...store.asyncReducers
    })
  );
};

Подключение lazy API slice

import { usersApi } from './usersApi';

store.injectReducer(usersApi.reducerPath, usersApi.reducer);
store.dispatch(usersApi.util.resetApiState());

3. Feature-based lazy API architecture

Наиболее масштабируемый подход — разделение API по feature-модулям.

Структура проекта

/api
  baseApi.js
/features
  users
    usersApi.js
    usersSlice.js
  auth
    authApi.js

usersApi.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}`
    })
  })
});

Lazy загрузка feature

export const loadUsersFeature = async () => {
  const module = await import('./usersApi');
  return module.usersApi;
};

4. Lazy API через code splitting в React Router

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 уже должны быть зарегистрированы заранее.


5. Предварительная регистрация API через loader

Часто используется паттерн bootstrap:

export const initApiModule = (api) => {
  store.injectReducer(api.reducerPath, api.reducer);
};

Lazy загрузка перед рендером

const module = await import('./features/users/usersApi');
initApiModule(module.usersApi);

Кеширование и shared state при lazy API

RTK Query использует единый cache key space внутри одного reducerPath.

При lazy загрузке важно:

  • не создавать дублирующие reducerPath
  • использовать общий baseApi
  • избегать повторной инициализации middleware

Ошибки архитектуры при lazy RTK Query

Дублирование API инстансов

Создание нескольких createApi с одинаковыми endpoints приводит к:

  • разному cache state
  • конфликту middleware
  • потере рефетчинга

Поздняя регистрация reducer

Если компонент уже использует useQuery, но reducer не подключён:

  • state будет undefined
  • hooks упадут с runtime error

Несогласованность injectEndpoints

Если один и тот же endpoint инжектится несколько раз:

  • возможны коллизии типов
  • перезапись query definitions

Оптимальная модель lazy API

Наиболее стабильная архитектура строится вокруг одного base API:

  • один createApi
  • множество injectEndpoints
  • динамический import feature-модулей
  • регистрация reducer до рендера маршрута

Практический паттерн: feature loader

export const loadFeature = async (featureName) => {
  switch (featureName) {
    case 'users':
      return import('./features/users/usersApi');
    case 'auth':
      return import('./features/auth/authApi');
  }
};

Централизованный API registry

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 RTK Query

Lazy API слой в RTK Query фактически представляет собой комбинацию:

  • createApi как базовый контракт
  • injectEndpoints как механизм расширения
  • dynamic import как механизм загрузки
  • store.replaceReducer как механизм интеграции в Redux
  • feature-based архитектуру как структурную основу

Такой подход позволяет масштабировать API слой без увеличения стартового бандла и без потери консистентности состояния кеша.