RTK Query строится вокруг идеи декларативного описания источников
данных и автоматизации всех этапов жизненного цикла запроса: выполнение,
кэширование, инвалидизация и повторное использование. При интеграции с
внешними сервисами ключевым элементом становится конфигурация
baseQuery, которая определяет способ коммуникации с
API.
Базовый вариант опирается на fetchBaseQuery,
представляющий собой тонкую обёртку над fetch с
добавленными возможностями сериализации, обработки заголовков и
ошибок.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com/v1',
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
});
Внешний сервис в этой модели рассматривается как источник REST-ресурсов, а каждый endpoint описывает конкретный контракт взаимодействия.
Практически любой внешний API требует авторизации: API key, JWT или
OAuth токены. В RTK Query это решается через
prepareHeaders, который позволяет динамически формировать
заголовки перед каждым запросом.
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com/v1',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
headers.set('x-api-key', process.env.API_KEY);
return headers;
},
});
Такой подход делает интеграцию с внешними сервисами централизованной: логика авторизации не размазывается по компонентам или thunk-слоям.
Сложные приложения часто взаимодействуют сразу с несколькими API: например, основной backend, сервис аналитики и сторонний геокодер. RTK Query позволяет реализовать это через несколько API-слайсов.
const mainApi = createApi({
reducerPath: 'mainApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
});
const geoApi = createApi({
reducerPath: 'geoApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://geo.external-service.com',
}),
endpoints: (builder) => ({
getCoordinates: builder.query({
query: (city) => `/coords?city=${city}`,
}),
}),
});
Такое разделение упрощает масштабирование и позволяет изолировать правила кэширования и инвалидации для каждого внешнего источника.
При интеграции с внешними сервисами часто возникает необходимость
унифицировать ошибки, логировать запросы или автоматически обновлять
токены. Для этого создаётся обёртка над базовым
fetchBaseQuery.
const rawBaseQuery = fetchBaseQuery({
baseUrl: 'https://api.example.com',
});
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await rawBaseQuery(args, api, extraOptions);
if (result.error && result.error.status === 401) {
const refreshResult = await rawBaseQuery(
'/auth/refresh',
api,
extraOptions
);
if (refreshResult.data) {
api.dispatch({ type: 'auth/setCredentials', payload: refreshResult.data });
result = await rawBaseQuery(args, api, extraOptions);
}
}
return result;
};
Такая прослойка превращает RTK Query в адаптер для нестабильных внешних API, где требуется автоматическое восстановление сессии.
Внешние сервисы редко придерживаются единого стандарта. Часто
требуется трансформация ответа до попадания в кэш RTK Query. Для этого
используется transformResponse.
getProducts: builder.query({
query: () => '/products',
transformResponse: (response) => {
return response.items.map((item) => ({
id: item.product_id,
title: item.name,
price: item.cost,
}));
},
});
Это позволяет отделить доменную модель приложения от формата внешнего API.
Многие внешние API используют query string вместо REST-структуры. RTK Query позволяет формировать параметры декларативно.
getSearchResults: builder.query({
query: ({ query, page }) => ({
url: '/search',
params: {
q: query,
page,
},
}),
});
Такой подход снижает риск ошибок при ручной конкатенации URL и упрощает поддержку сложных фильтров.
Хотя RTK Query ориентирован на REST, он легко адаптируется под
GraphQL через кастомный baseQuery.
const graphqlBaseQuery =
({ baseUrl }) =>
async ({ body }) => {
const result = await fetch(baseUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await result.json();
if (data.errors) {
return { error: data.errors };
}
return { data: data.data };
};
Использование:
const api = createApi({
reducerPath: 'graphqlApi',
baseQuery: graphqlBaseQuery({
baseUrl: 'https://graphql.example.com',
}),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => ({
body: {
query: `
query ($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id },
},
}),
}),
}),
});
При интеграции с внешними сервисами критически важно контролировать
жизненный цикл кэша. RTK Query использует tagTypes для
связи данных и операций инвалидизации.
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Users', 'Posts'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['Users'],
}),
addUser: builder.mutation({
query: (user) => ({
url: '/users',
method: 'POST',
body: user,
}),
invalidatesTags: ['Users'],
}),
}),
});
При работе с внешними сервисами, где данные могут изменяться вне контроля приложения, часто требуется агрессивная инвалидизация или отключение кэширования для критичных endpoints.
Интеграция с внешними сервисами почти всегда требует разделения окружений: development, staging, production.
baseQuery: fetchBaseQuery({
baseUrl: process.env.NODE_ENV === 'production'
? 'https://api.prod.com'
: 'https://api.dev.com',
});
В более сложных сценариях baseUrl может зависеть от параметров запроса:
const dynamicBaseQuery = async (args, api, extraOptions) => {
const service = args.service;
const baseUrlMap = {
main: 'https://api.main.com',
analytics: 'https://analytics.external.com',
};
const baseQuery = fetchBaseQuery({
baseUrl: baseUrlMap[service],
});
return baseQuery(args, api, extraOptions);
};
Некоторые внешние API предоставляют потоковые данные. RTK Query
поддерживает такой сценарий через onCacheEntryAdded.
getLiveUpdates: builder.query({
query: () => '/stream',
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
await cacheDataLoaded;
const ws = new WebSocket('wss://api.example.com/live');
ws.onmess age = (event) => {
const data = JSON.parse(event.data);
updateCachedData((draft) => {
draft.push(data);
});
};
await cacheEntryRemoved;
ws.close();
},
});
Таким образом RTK Query превращается в мост между внешними потоковыми сервисами и локальным кэшем приложения.
Внешние сервисы часто возвращают разные форматы ошибок. Для
унификации используется слой нормализации внутри
baseQuery.
const normalizedBaseQuery = async (args, api, extraOptions) => {
const result = await fetchBaseQuery({
baseUrl: 'https://api.example.com',
})(args, api, extraOptions);
if (result.error) {
return {
error: {
status: result.error.status,
message: result.error.data?.message || 'Unknown error',
code: result.error.data?.code,
},
};
}
return result;
};
Такой слой обеспечивает стабильный контракт между внешними сервисами и внутренней логикой приложения.
Внешние API часто требуют multipart-загрузку. RTK Query позволяет
реализовать это через FormData.
uploadFile: builder.mutation({
query: (file) => {
const formData = new FormData();
formData.append('file', file);
return {
url: '/upload',
method: 'POST',
body: formData,
};
},
});
Важно учитывать, что в этом случае заголовок
Content-Type не задаётся вручную — браузер формирует его
автоматически.
В сложных приложениях данные могут агрегироваться из нескольких API
одновременно. RTK Query позволяет комбинировать запросы через
queryFn.
getDashboardData: builder.query({
async queryFn(arg, api, extraOptions, baseQuery) {
const [users, stats] = await Promise.all([
baseQuery('/users', api, extraOptions),
baseQuery('/stats', api, extraOptions),
]);
if (users.error || stats.error) {
return { error: 'Failed to load dashboard' };
}
return {
data: {
users: users.data,
stats: stats.data,
},
};
},
});
Такой подход используется при интеграции с внешними сервисами, где требуется агрегация данных на клиенте.
Внешние сервисы часто нестабильны. RTK Query позволяет реализовать retry-логику через кастомный baseQuery.
const baseQueryWithRetry = async (args, api, extraOptions) => {
let result;
for (let i = 0; i < 3; i++) {
result = await fetchBaseQuery({
baseUrl: 'https://api.example.com',
})(args, api, extraOptions);
if (!result.error) break;
}
return result;
};
Некоторые внешние сервисы требуют строгой последовательности операций. RTK Query позволяет управлять этим через комбинацию мутаций и инвалидизации кэша, исключая необходимость ручного управления состоянием.
updateProfile: builder.mutation({
query: (data) => ({
url: '/profile',
method: 'PATCH',
body: data,
}),
invalidatesTags: ['Profile'],
});
Такая модель снижает связанность кода с внешними API и делает интеграцию предсказуемой даже при сложной бизнес-логике.