RTK Query предоставляет высокоуровневую абстракцию над сетевым слоем, кэшированием и синхронизацией состояния, однако его архитектура изначально рассчитана на глубокую адаптацию под конкретные требования приложения. Кастомизация затрагивает несколько уровней: поведение API-слайса, конфигурацию эндпоинтов, управление кэшем, обработку запросов, интеграцию с middleware, а также расширение через baseQuery и lifecycle-хуки.
Основной механизм настройки поведения RTK Query — функция
createApi. Именно здесь определяется фундаментальная логика
работы всего API-слоя.
Ключевые параметры конфигурации:
reducerPath — имя слайса в Redux storebaseQuery — низкоуровневый слой выполнения
запросовtagTypes — система инвалидирования кэшаendpoints — набор запросов и мутацийrefetchOnMountOrArgChange — глобальное поведение
повторных запросовkeepUnusedDataFor — время жизни неиспользуемого
кэшаПример базовой конфигурации:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
tagTypes: ['User', 'Post'],
keepUnusedDataFor: 30,
refetchOnMountOrArgChange: true,
endpoints: () => ({})
});
Каждый из этих параметров влияет на поведение системы кэширования и жизненного цикла данных.
baseQuery — это стратегический уровень кастомизации,
позволяющий полностью переопределить способ выполнения запросов.
По умолчанию используется fetchBaseQuery, который
представляет собой обёртку над fetch с добавленной логикой
сериализации, обработки ошибок и заголовков.
Однако в реальных приложениях часто требуется:
Наиболее распространённый подход — обёртка над
fetchBaseQuery:
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const rawBaseQuery = fetchBaseQuery({
baseUrl: '/api',
});
export const baseQueryWithAuth = async (args, api, extraOptions) => {
const token = api.getState().auth.token;
const result = await rawBaseQuery(
{
...args,
headers: {
...args.headers,
Authorization: token ? `Bearer ${token}` : '',
},
},
api,
extraOptions
);
return result;
};
Здесь поведение запроса модифицируется без изменения логики RTK Query.
RTK Query позволяет реализовать собственный транспортный слой:
const customBaseQuery = async (args, api, extraOptions) => {
try {
const response = await customHttpClient.request(args);
return { data: response.data };
} catch (error) {
return {
error: {
status: error.status,
data: error.message,
},
};
}
};
Такой подход используется при интеграции:
Каждый endpoint в RTK Query может иметь собственную логику, отличающуюся от глобальной.
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `/users/${id}`,
}),
updateUser: builder.mutation({
query: (body) => ({
url: `/users/${body.id}`,
method: 'PUT',
body,
}),
}),
})
Однако ключевая сила заключается в дополнительных параметрах:
transformResponsetransformErrorResponseprovidesTagsinvalidatesTagskeepUnusedDataForonQueryStartedПозволяет адаптировать API-ответ под структуру приложения.
getUser: builder.query({
query: (id) => `/users/${id}`,
transformResponse: (response) => response.data.user,
});
Это позволяет изолировать UI от особенностей backend-структуры.
transformErrorResponse: (response) => {
return {
message: response.data?.message || 'Ошибка запроса',
status: response.status,
};
}
Такой слой используется для нормализации ошибок независимо от API.
Система тегов — центральный механизм инвалидации данных.
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
});
createPost: builder.mutation({
query: (post) => ({
url: '/posts',
method: 'POST',
body: post,
}),
invalidatesTags: ['Post'],
});
providesTags: (result) =>
result
? result.map(({ id }) => ({ type: 'Post', id }))
: ['Post'];
Это позволяет выполнять точечную инвалидацию.
onQueryStarted позволяет вмешиваться в процесс запроса
на этапе его запуска.
updateUser: builder.mutation({
query: (user) => ({
url: `/users/${user.id}`,
method: 'PATCH',
body: user,
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
} catch (err) {
console.error('Ошибка обновления', err);
}
},
});
Один из наиболее мощных сценариев кастомизации:
updateUser: builder.mutation({
query: (user) => ({
url: `/users/${user.id}`,
method: 'PATCH',
body: user,
}),
async onQueryStarted(user, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getUser', user.id, (draft) => {
Object.assign(draft, user);
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
});
Здесь происходит локальное изменение кэша до подтверждения сервера.
RTK Query хранит данные в нормализованном кэше, поведение которого можно контролировать.
keepUnusedDataFor: 60
Определяет, сколько секунд данные остаются в памяти после того, как последний подписчик отписался.
refetchOnMountOrArgChange: true
Определяет, нужно ли повторно загружать данные при повторном монтировании компонента.
Дополнительная локальная переопределяемость:
useGetUserQuery(id, {
refetchOnMountOrArgChange: false,
});
Глобальные стратегии актуализации данных:
refetchOnFocus: true,
refetchOnReconnect: true
Они обеспечивают синхронизацию данных при возвращении пользователя в приложение или восстановлении сети.
RTK Query предоставляет низкоуровневые инструменты управления кэшем.
dispatch(api.util.invalidateTags(['Post']));
Полный сброс состояния API:
dispatch(api.util.resetApiState());
Используется при logout или смене пользователя.
RTK Query поддерживает автоматическое обновление данных через polling:
useGetMessagesQuery(undefined, {
pollingInterval: 5000,
});
Это поведение можно динамически управлять:
Механизм условного выполнения запросов:
useGetUserQuery(id, {
skip: !id,
});
Позволяет полностью контролировать момент запуска запроса.
Позволяет ограничить пересчёт компонентов и оптимизировать ререндер:
useGetUserQuery(id, {
selectFromResult: ({ data, isLoading }) => ({
user: data,
loading: isLoading,
}),
});
Это снижает нагрузку на UI при частых обновлениях кэша.
RTK Query интегрируется в Redux middleware pipeline, что позволяет расширять поведение:
Пример добавления middleware:
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
RTK Query поддерживает паттерн динамической генерации API:
export const createDynamicApi = (baseUrl) =>
createApi({
reducerPath: 'dynamicApi',
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getData: builder.query({
query: () => '/',
}),
}),
});
Это полезно для мульти-доменных систем.
extraOptions передаются в baseQuery и
позволяют реализовать пользовательские режимы обработки:
const baseQuery = fetchBaseQuery({ baseUrl: '/api' });
const enhancedBaseQuery = async (args, api, extraOptions) => {
if (extraOptions?.silent) {
// отключение глобальных индикаторов загрузки
}
return baseQuery(args, api, extraOptions);
};
RTK Query позволяет строить сложные UI-сценарии через комбинацию:
isLoadingisFetchingisSuccessisErrorЭти флаги могут быть переопределены через кастомные селекторы и
selectFromResult, что позволяет тонко контролировать UX без
изменения бизнес-логики.
Кастомизация часто используется для разделения dev/prod поведения:
const baseQuery = fetchBaseQuery({
baseUrl: process.env.NODE_ENV === 'production'
? 'https://api.site.com'
: 'http://localhost:3000',
});
Также возможно добавление логирования только в dev:
if (process.env.NODE_ENV === 'development') {
console.log('Request:', args);
}
Ключевой паттерн кастомизации — композиция:
Каждый слой добавляет независимую ответственность, сохраняя предсказуемость поведения системы.
RTK Query позволяет выносить бизнес-логику в:
transformResponseonQueryStartedЭто снижает связность компонентов и API-слоя, обеспечивая централизованное управление поведением данных.