injectEndpoints

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

Базовая идея расширяемого API

Вместо того чтобы создавать единый монолитный API-файл, RTK Query позволяет разделять описание эндпоинтов по функциональным модулям. Каждый модуль может «вкалывать» свои эндпоинты в общий API-слой.

Ключевой момент заключается в том, что injectEndpoints не создаёт новый API, а расширяет существующий. Это означает:

  • сохраняется общий cache key namespace
  • используется один и тот же reducer
  • middleware остаётся единым
  • RTK Query сохраняет целостность кэша

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


Синтаксис injectEndpoints

Метод вызывается на уже созданном API-сервисе:

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

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

  • api — базовый API, созданный через createApi
  • injectEndpoints — метод расширения
  • endpoints — функция, принимающая builder
  • builder.query / builder.mutation — описание операций

Важно, что возвращаемое значение — новый объект API с добавленными эндпоинтами, но внутренне он связан с исходным сервисом.


Поведение при множественных инъекциях

Каждый вызов injectEndpoints может добавлять новые эндпоинты к уже существующим.

const usersApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',
    }),
  }),
});

const postsApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
    }),
  }),
});

Оба набора эндпоинтов оказываются в одном API-контексте. Это означает:

  • общий cache store
  • единый lifecycle подписок
  • отсутствие дублирования reducer-логики

Стратегия использования с code splitting

Одно из ключевых применений injectEndpoints — разделение API по чанкам приложения.

Пример модульной структуры

api/
  baseApi.js
features/
  users/
    usersApi.js
  posts/
    postsApi.js

baseApi.js

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

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

Здесь важно, что endpoints пустой — API создаётся как контейнер.


usersApi.js

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

export const usersApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',
    }),
  }),
});

export const { useGetUsersQuery } = usersApi;

postsApi.js

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

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

export const { useGetPostsQuery } = postsApi;

Переиспользование baseApi и стабильность ссылок

Несмотря на динамическое добавление эндпоинтов, api остаётся стабильным объектом. Это критично для React-интеграции:

  • store содержит один reducer
  • middleware не пересоздаётся
  • RTK Query сохраняет кэш между модулями

Даже если injectEndpoints вызывается в разных местах, результат агрегируется в одном API namespace.


overrideExisting и управление конфликтами

При инъекции возможны конфликты имён эндпоинтов. Для этого используется параметр overrideExisting.

api.injectEndpoints({
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users/v2',
    }),
  }),
  overrideExisting: true,
});

Поведение параметра

  • true — перезаписывает существующий эндпоинт
  • false (по умолчанию) — вызывает предупреждение или ошибку в dev-режиме

Это важно при миграциях API или feature toggling.


Ленивая загрузка эндпоинтов

injectEndpoints часто используется вместе с динамическим импортом:

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

В этом сценарии эндпоинты регистрируются только при загрузке соответствующего модуля, что уменьшает initial bundle size.


Типизация и автогенерация хуков

RTK Query автоматически генерирует React hooks для injected endpoints:

export const { useGetUsersQuery } = usersApi;

Особенность injectEndpoints заключается в том, что типы выводятся из builder-описания, а не из центрального API-файла. Это позволяет:

  • локализовать типизацию внутри feature-модуля
  • избежать зависимости от монолитного API типа
  • сохранять масштабируемость проекта

Взаимодействие с кэшем RTK Query

Несмотря на модульное расширение, все injected endpoints работают с единой системой кэширования:

  • одинаковые baseQuery
  • единый reducerPath
  • общий normalized cache

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

Пример:

  • getUsers в usersApi
  • getUserById в postsApi или другом модуле

Если queryKey совпадает логически, RTK Query использует кэш повторно.


Ограничения injectEndpoints

Несмотря на гибкость, механизм имеет ряд архитектурных ограничений:

1. Невозможность смены базовой конфигурации

После создания createApi нельзя изменить:

  • baseQuery
  • reducerPath
  • tagTypes

injectEndpoints работает только на уровне эндпоинтов.


2. Жёсткая привязка к одному API-инстансу

Все injected endpoints должны принадлежать одному API. Нельзя объединить несколько разных createApi через inject.


3. Потенциальные конфликты имен

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

  • getUsers
  • users/getUsers (не поддерживается автоматически)
  • уникальные ключи обязательны

Практика организации feature-based API

Наиболее устойчивый подход — привязка injectEndpoints к feature-слою:

features/
  auth/
    authApi.js
  profile/
    profileApi.js
  dashboard/
    dashboardApi.js

Каждый модуль:

  • инжектирует свои endpoints
  • экспортирует hooks
  • не зависит от других feature-модулей

Это создаёт слабую связанность между частями API.


Интеграция с store

Базовый store конфигурируется один раз:

import { configureStore } from '@reduxjs/toolkit';
import { api } from './api/baseApi';

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
});

После этого любые injectEndpoints автоматически начинают работать без дополнительной настройки store.


Динамическая регистрация в рантайме

При необходимости endpoints могут добавляться уже после инициализации приложения:

store.dispatch(
  api.util.resetApiState()
);

или через side-effect загрузку модулей:

await import('./features/comments/commentsApi');

После импорта endpoints становятся доступными в API-контексте без перезагрузки store.


Поведение dev-tools и отладка

Redux DevTools отображают injected endpoints так же, как и статические:

  • единый API slice
  • единый action namespace
  • одинаковая структура cache state

Однако при большом количестве inject-модулей важно контролировать:

  • дублирование endpoint names
  • неожиданные cache invalidations
  • порядок загрузки модулей

Архитектурная роль injectEndpoints

Механизм injectEndpoints фактически превращает RTK Query в:

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

Он позволяет уходить от централизованных API-файлов в сторону распределённой архитектуры, где каждый feature владеет своими запросами, но использует общий кэш и транспортный слой.