В крупных приложениях RTK Query перестаёт быть просто «слоем для запросов» и превращается в центральную часть инфраструктуры работы с серверным состоянием. Ошибки в организации кода здесь быстро приводят к дублированию эндпоинтов, размытию ответственности, сложной поддержке и конфликтам между модулями.
Ключевая цель архитектуры RTK Query в больших проектах — обеспечить:
В масштабируемой архитектуре всегда выделяется один базовый API-инстанс, который становится фундаментом для всех последующих расширений.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
endpoints: () => ({}),
});
Этот слой принципиально не должен содержать бизнес-эндпоинтов. Его задача:
Такой подход предотвращает монолитный API-файл и позволяет строить модульную систему.
В крупных системах домены становятся основным способом структурирования RTK Query.
Типичные домены:
Каждый домен оформляется как отдельный модуль, который расширяет базовый API.
src/
app/
store.js
baseApi.js
features/
users/
usersApi.js
usersTypes.js
products/
productsApi.js
orders/
ordersApi.js
Такое разделение исключает смешивание логики и упрощает навигацию по проекту.
Основной механизм расширения RTK Query —
injectEndpoints. Он позволяет добавлять эндпоинты без
изменения базового API.
import { baseApi } from '../. ./app/baseApi';
export const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
});
export const { useGetUsersQuery } = usersApi;
Особенность подхода заключается в том, что:
Каждый модуль RTK Query должен содержать только:
Не допускается:
export const productsApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getProducts: builder.query({
query: () => '/products',
providesTags: ['Products'],
}),
getProductById: builder.query({
query: (id) => `/products/${id}`,
providesTags: (result, error, id) => [{ type: 'Products', id }],
}),
}),
});
В больших приложениях кеширование становится критическим фактором стабильности. Без строгой системы тегов возникает неконтролируемая инвалидация.
Рекомендуется вводить:
export const TAGS = {
USERS: 'Users',
PRODUCTS: 'Products',
ORDERS: 'Orders',
};
Использование:
providesTags: [TAGS.PRODUCTS]
Такой подход снижает вероятность конфликтов и облегчает рефакторинг.
В больших системах важно чётко отделять:
Это не только логическая, но и архитектурная граница.
endpoints: (builder) => ({
getOrders: builder.query({
query: () => '/orders',
providesTags: ['Orders'],
}),
createOrder: builder.mutation({
query: (body) => ({
url: '/orders',
method: 'POST',
body,
}),
invalidatesTags: ['Orders'],
}),
});
Разделение обеспечивает:
Одна из ключевых проблем больших проектов — повторение одинаковых запросов в разных модулях.
Решение:
const buildUrl = (path) => `/api/v1/${path}`;
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => {
headers.set('authorization', `Bearer token`);
return headers;
},
});
const listQuery = (resource) => ({
query: () => `/${resource}`,
});
В сложных системах иногда возникает необходимость разделения API на несколько инстансов:
export const publicApi = createApi({
reducerPath: 'publicApi',
baseQuery: fetchBaseQuery({ baseUrl: '/public' }),
endpoints: () => ({}),
});
export const privateApi = createApi({
reducerPath: 'privateApi',
baseQuery: fetchBaseQuery({ baseUrl: '/private' }),
endpoints: () => ({}),
});
Однако злоупотребление этим подходом приводит к:
Поэтому разделение оправдано только при чёткой архитектурной необходимости.
В больших приложениях подключение API должно быть строго структурировано.
import { configureStore } from '@reduxjs/toolkit';
import { baseApi } from './baseApi';
export const store = configureStore({
reducer: {
[baseApi.reducerPath]: baseApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
});
При использовании injectEndpoints дополнительных
reducer’ов не требуется — это критически важный момент, который часто
нарушается в плохо организованных проектах.
Каждый доменный модуль должен иметь предсказуемую структуру.
users/
api/
usersApi.js
model/
selectors.js
types.js
ui/
UsersList.jsx
UserCard.jsx
Такое разделение обеспечивает:
RTK Query поддерживает ленивую регистрацию эндпоинтов, что особенно важно для больших приложений с code splitting.
const usersApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
overrideExisting: false,
});
При использовании динамических импортов:
const loadUsersApi = async () => {
const module = await import('./usersApi');
return module;
};
Это позволяет:
В больших системах критично избегать циклических зависимостей между API-модулями.
Типичная ошибка:
RTK Query не предназначен для междоменных вызовов внутри endpoints. Решение:
RTK Query предоставляет селекторы кеша, которые можно использовать для построения сложных вычисляемых данных без дополнительных запросов.
export const selectUsersResult = usersApi.endpoints.getUsers.select();
export const selectActiveUsers = (state) => {
const result = selectUsersResult(state);
return result?.data?.filter(user => user.active);
};
Такой подход:
При росте проекта критически важно избегать хаоса в именах.
Рекомендуемая схема:
Дополнительно:
В крупных приложениях инвалидация становится сложной задачей, особенно при перекрёстных зависимостях.
Рекомендуемая стратегия:
invalidatesTags: ['Orders']
А не:
invalidatesTags: [{ type: 'Orders', id: '123' }]
если нет строгой необходимости точечной инвалидации.
RTK Query должен оставаться инфраструктурным слоем, а не местом хранения бизнес-логики.
Недопустимо:
Допустимо:
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => response.data,
});
Этот механизм должен использоваться умеренно:
В системах, работающих годами, RTK Query становится частью архитектурного ядра. Поэтому критичны:
Структурированный подход к организации кода напрямую влияет на стоимость дальнейшей поддержки и расширения системы.