Одним из ключевых специфичных кейсов в RTK Query является управление тем, когда именно запрос должен выполняться. В реальных приложениях данные часто зависят от состояния интерфейса, наличия параметров или внешних условий, и выполнение запроса «всегда» приводит к лишним сетевым вызовам и усложнению логики.
skipRTK Query позволяет полностью контролировать выполнение запроса с
помощью флага skip. Это особенно полезно, когда данные ещё
не готовы для запроса.
const { data, isLoading } = useGetUserQuery(userId, {
skip: !userId
});
Если userId отсутствует, запрос не выполняется, а RTK
Query не создаёт подписку на этот endpoint.
Типичный кейс:
skipToken как
более строгий вариантskipToken используется в случаях, когда нужно явно
сигнализировать системе, что запрос должен быть полностью пропущен на
уровне аргументов.
import { skipToken } from '@reduxjs/toolkit/query';
const { data } = useGetUserQuery(userId ?? skipToken);
Этот подход особенно важен при динамическом формировании аргументов,
где undefined может быть валидным значением.
В реальных API часто встречается ситуация, когда один запрос зависит от результата другого. Например, сначала нужно получить профиль пользователя, а затем его заказы.
const { data: user } = useGetUserQuery(userId);
const { data: orders } = useGetOrdersQuery(user?.id ?? skipToken);
Здесь второй запрос запускается только после получения
user.id.
Основная сложность таких цепочек:
userIdRTK Query решает часть этих проблем через нормализацию кеша и дедупликацию, но архитектурно важно избегать чрезмерной вложенности зависимостей.
В приложениях с быстрыми изменениями параметров (поиск, фильтры, автокомплит) часто возникает ситуация, когда более старый запрос приходит позже нового.
RTK Query автоматически отменяет устаревшие запросы через
abortController, но есть нюансы.
const { data } = useSearchQuery(query);
При вводе:
запросы отправляются последовательно, но ответы могут приходить в обратном порядке.
queryCacheKeyОднако при кастомном baseQuery важно корректно
обрабатывать AbortError, иначе возможны утечки
состояния.
При использовании fetchBaseQuery отмена обрабатывается
автоматически, но при кастомных реализациях нужно учитывать сигнал.
const baseQuery = async (args, api, extraOptions) => {
const { signal } = api;
const response = await fetch(args.url, { signal });
if (!response.ok) {
return { error: { status: response.status } };
}
return { data: await response.json() };
};
Игнорирование signal приводит к ситуации, когда уже
отменённый запрос всё равно мутирует состояние.
RTK Query использует аргументы запроса как часть ключа кеширования. Это означает, что любые нестабильные ссылки могут привести к избыточным запросам.
useGetPostsQuery({ filters: { tag: 'news' } });
Если объект создаётся заново при каждом рендере, RTK Query воспринимает его как новый ключ.
Стабилизация аргументов:
const filters = useMemo(() => ({
tag: 'news'
}), []);
useGetPostsQuery(filters);
Или использование сериализуемых примитивов.
В сложных системах стандартная сериализация может быть недостаточной.
getPosts: builder.query({
query: (params) => ({
url: '/posts',
params
}),
serializeQueryArgs: ({ endpointName, queryArgs }) => {
return `${endpointName}-${queryArgs.tag}`;
}
});
Базовая модель тегов работает хорошо, пока система проста. В реальных приложениях часто требуется более тонкая настройка.
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
Когда один endpoint влияет на несколько других:
RTK Query позволяет указывать множественные
invalidatesTags, но важно избегать каскадной инвалидации,
приводящей к лавинообразным перезапросам.
Оптимистические обновления часто используются для мгновенного UI-отклика, но требуют аккуратного управления состоянием кеша.
updatePost: builder.mutation({
query: (post) => ({
url: `/posts/${post.id}`,
method: 'PUT',
body: post
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', arg.id, (draft) => {
Object.assign(draft, arg);
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
}
});
Один из самых сложных кейсов — корректная работа кеша при пагинации.
useGetPostsQuery({ page: 1 });
useGetPostsQuery({ page: 2 });
Каждая страница хранится отдельно, что усложняет объединение данных.
merge: (currentCache, newItems) => {
currentCache.push(...newItems);
}
Без правильного serializeQueryArgs возможны дубли
страниц и некорректное обновление списка.
RTK Query не ограничивается HTTP-запросами и может интегрироваться с потоковыми данными.
onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const ws = new WebSocket('wss://example.com');
ws.onmess age = (event) => {
updateCachedData((draft) => {
draft.push(JSON.parse(event.data));
});
};
await cacheEntryRemoved;
ws.close();
}
Многие API возвращают ошибки не в стандартном HTTP-формате.
baseQuery: fetchBaseQuery({
baseUrl: '/api',
responseHandler: async (response) => {
const data = await response.json();
if (data.error) {
return { error: { message: data.error.message } };
}
return { data };
}
});
RTK Query позволяет точечно изменять данные без повторного запроса.
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === 1);
if (post) {
post.title = 'Updated';
}
})
);
Важно синхронизировать такие обновления с серверными мутациями, иначе возможны рассинхронизации при refetch.
В мобильных и слабых сетях появляются специфические сценарии:
RTK Query частично решает это через:
Однако устойчивость системы в таких условиях сильно зависит от
архитектуры API слоя и корректной работы baseQuery.