Обработка специфичных кейсов

Одним из ключевых специфичных кейсов в RTK Query является управление тем, когда именно запрос должен выполняться. В реальных приложениях данные часто зависят от состояния интерфейса, наличия параметров или внешних условий, и выполнение запроса «всегда» приводит к лишним сетевым вызовам и усложнению логики.

Пропуск запроса через skip

RTK 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 может быть валидным значением.


Зависимые (chained) запросы

В реальных API часто встречается ситуация, когда один запрос зависит от результата другого. Например, сначала нужно получить профиль пользователя, а затем его заказы.

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

const { data: user } = useGetUserQuery(userId);

const { data: orders } = useGetOrdersQuery(user?.id ?? skipToken);

Здесь второй запрос запускается только после получения user.id.

Специфика проблем

Основная сложность таких цепочек:

  • возможные лишние перезапросы при изменении первого результата
  • гонки состояний при быстрых переключениях userId
  • необходимость кеширования промежуточных данных

RTK Query решает часть этих проблем через нормализацию кеша и дедупликацию, но архитектурно важно избегать чрезмерной вложенности зависимостей.


Управление гонками запросов (race conditions)

В приложениях с быстрыми изменениями параметров (поиск, фильтры, автокомплит) часто возникает ситуация, когда более старый запрос приходит позже нового.

RTK Query автоматически отменяет устаревшие запросы через abortController, но есть нюансы.

Пример проблемы

const { data } = useSearchQuery(query);

При вводе:

  • “a”
  • “ap”
  • “app”

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

Как RTK 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}`;
  }
});

Когда это необходимо

  • игнорирование части параметров (например, pagination metadata)
  • объединение нескольких фильтров в один кеш-контекст
  • предотвращение дублирования кешей при эквивалентных запросах

Инвалидация кеша в сложных сценариях

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

Перекрёстная инвалидация

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();
    }
  }
});

Специфические проблемы

  • несоответствие серверного и локального состояния
  • повторные мутации при ретраях
  • конкуренция нескольких optimistic updates

Пагинация и бесконечные списки

Один из самых сложных кейсов — корректная работа кеша при пагинации.

Проблема раздельного кеша

useGetPostsQuery({ page: 1 });
useGetPostsQuery({ page: 2 });

Каждая страница хранится отдельно, что усложняет объединение данных.

Объединение страниц в один кеш

merge: (currentCache, newItems) => {
  currentCache.push(...newItems);
}

Важный нюанс

Без правильного serializeQueryArgs возможны дубли страниц и некорректное обновление списка.


Работа с реальным временем и WebSocket

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();
}

Специфика

  • управление жизненным циклом соединения
  • предотвращение утечек WebSocket при размонтировании
  • синхронизация потоковых и REST-данных

Обработка ошибок в нестандартных форматах API

Многие 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 };
  }
});

Сложные случаи

  • вложенные ошибки
  • смешанные форматы (HTTP + бизнес-ошибки)
  • необходимость унификации error shape для UI

Частичное обновление кеша

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 частично решает это через:

  • deduplication
  • retry logic (в кастомном baseQuery)
  • cache persistence

Однако устойчивость системы в таких условиях сильно зависит от архитектуры API слоя и корректной работы baseQuery.