Динамическое добавление endpoints

Архитектура RTK Query построена вокруг идеи декларативного описания API через единый apiSlice, который создаётся с помощью createApi. Однако в реальных приложениях часто возникает необходимость расширять API после его первоначального определения: подключать новые наборы запросов в зависимости от фичи, разделять код по модулям или подгружать функциональность лениво. Для этого используется механизм динамического расширения через injectEndpoints.


Базовый принцип injectEndpoints

Функция injectEndpoints позволяет добавлять новые endpoints в уже существующий API-слайс без его пересоздания. Это ключевой механизм для модульной архитектуры и code splitting.

Основная идея:

  • существует базовый apiSlice
  • в отдельных модулях описываются дополнительные endpoints
  • они “вкалываются” в общий API через injectEndpoints

Это позволяет избегать монолитного описания API и поддерживать масштабируемость.


Создание базового API-слайса

Перед использованием динамического расширения создаётся основной API:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

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

Ключевая деталь — endpoints: () => ({}). Пустое описание endpoints означает, что базовый слой только готовит инфраструктуру для дальнейшего расширения.


Добавление endpoints через injectEndpoints

Динамическое расширение происходит следующим образом:

import { baseApi } from './baseApi';

export const extendedApi = baseApi.injectEndpoints({
  endpoints: (build) => ({
    getUsers: build.query({
      query: () => '/users',
    }),
  }),
  overrideExisting: false,
});

Основные элементы:

  • endpoints: функция, принимающая build и возвращающая объект endpoints
  • build.query: описание GET-подобного запроса
  • build.mutation: описание изменений состояния на сервере
  • overrideExisting: управление конфликтами имён

Ленивая модульная структура

Часто injectEndpoints используется без создания нового API-объекта. Сам вызов достаточно выполнить один раз, чтобы зарегистрировать endpoints глобально.

// usersApi.js
import { baseApi } from './baseApi';

baseApi.injectEndpoints({
  endpoints: (build) => ({
    getUsers: build.query({
      query: () => '/users',
    }),
  }),
});

После выполнения этого кода endpoint становится доступен через baseApi.


Разделение API по доменам

В крупных приложениях endpoints группируются по функциональным зонам:

usersApi

import { baseApi } from '../baseApi';

export const usersApi = baseApi.injectEndpoints({
  endpoints: (build) => ({
    getUserById: build.query({
      query: (id) => `/users/${id}`,
    }),
    updateUser: build.mutation({
      query: (body) => ({
        url: `/users/${body.id}`,
        method: 'PUT',
        body,
      }),
    }),
  }),
});

postsApi

import { baseApi } from '../baseApi';

export const postsApi = baseApi.injectEndpoints({
  endpoints: (build) => ({
    getPosts: build.query({
      query: () => '/posts',
    }),
  }),
});

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


Генерация React hooks

RTK Query автоматически генерирует хуки для каждого endpoint. При динамическом добавлении это работает аналогично статическим endpoints.

export const {
  useGetUsersQuery,
  useUpdateUserMutation,
} = usersApi;

Важно учитывать, что хуки доступны только после того, как модуль с injectEndpoints был импортирован хотя бы один раз.


Ленивая загрузка endpoints (code splitting)

Динамическое добавление особенно полезно в связке с lazy loading:

import React, { useEffect } from 'react';
import { baseApi } from './baseApi';

export function UsersPage() {
  useEffect(() => {
    import('./usersApi');
  }, []);

  const { data } = baseApi.useGetUsersQuery();

  return (
    <div>
      {data?.map((u) => (
        <div key={u.id}>{u.name}</div>
      ))}
    </div>
  );
}

Здесь:

  • usersApi подгружается только при открытии страницы
  • endpoints регистрируются в runtime
  • основной bundle не содержит лишнего API-кода

overrideExisting и конфликты endpoints

При повторной инъекции endpoint с тем же именем возникает конфликт. Управление происходит через overrideExisting.

Поведение:

  • false — ошибка или игнорирование повторного определения
  • true — перезапись существующего endpoint
baseApi.injectEndpoints({
  endpoints: (build) => ({
    getUsers: build.query({
      query: () => '/users-v2',
    }),
  }),
  overrideExisting: true,
});

Этот механизм используется при версионировании API или feature flags.


Расширение существующих типов данных

injectEndpoints позволяет расширять не только запросы, но и поведение кэширования, теги и инвалидацию.

baseApi.injectEndpoints({
  endpoints: (build) => ({
    addUser: build.mutation({
      query: (user) => ({
        url: '/users',
        method: 'POST',
        body: user,
      }),
      invalidatesTags: ['Users'],
    }),
  }),
});

При этом важно, что теговая система остаётся общей для всего API-слайса.


Совместное использование baseApi между модулями

Один baseApi становится центральной точкой регистрации всех endpoints. Модули могут импортировать его независимо друг от друга.

// module A
baseApi.injectEndpoints({ ... });

// module B
baseApi.injectEndpoints({ ... });

RTK Query гарантирует объединение всех endpoints в единый registry.


Поведение кэша при динамическом добавлении

Кэш RTK Query не зависит от способа добавления endpoint. Однако есть важный момент:

  • запросы, вызванные до инъекции endpoint, не активируются автоматически
  • endpoint должен быть зарегистрирован до использования соответствующего hook

Это критично при lazy loading: порядок импорта влияет на доступность данных.


Типичные архитектурные ошибки

1. Создание нескольких createApi

Часто вместо injectEndpoints создают несколько createApi. Это приводит к:

  • дублированию store middleware
  • разделению кэша
  • усложнению синхронизации

Правильный подход — один baseApi + injectEndpoints.


2. Отсутствие гарантированного импорта модуля

Если модуль с injectEndpoints не загружен, hook не существует:

  • ошибка useXxxQuery is not a function
  • отсутствие регистрации endpoint

Решается явным импортом или lazy import на уровне маршрутизации.


3. Конфликты имен

При одинаковых именах endpoints:

  • теряется предсказуемость API
  • ломается генерация хуков

Рекомендуется использовать нейминг по доменам:

getUserById
users/getById
auth/login

Интеграция с TypeScript (если используется)

Хотя пример приведён в JavaScript, динамические endpoints также поддерживают типизацию. Типы автоматически расширяются при корректной организации модулей, но требуют аккуратной структуры импортов, чтобы избежать расхождения типов и runtime-реальности.


Поведение middleware и store

После добавления endpoints через injectEndpoints:

  • middleware RTK Query остаётся неизменным
  • reducer не пересоздаётся
  • добавляется только описание новых запросов

Это обеспечивает стабильность store даже при динамическом расширении API.


Использование в feature-based архитектуре

Наиболее естественное применение injectEndpoints — разбиение по фичам:

/features
  /users
    usersApi.js
    usersSlice.js
  /posts
    postsApi.js
    postsSlice.js

Каждый модуль самостоятельно расширяет baseApi, не зная о других частях системы.