Параллельные запросы

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

Принцип независимости запросов

Каждый вызов хука RTK Query формирует отдельную подписку на данные. Ключевая особенность заключается в том, что каждый endpoint идентифицируется комбинацией:

  • имени endpoint’а
  • аргументов запроса

Это означает, что даже внутри одного компонента можно безопасно запускать несколько запросов без риска конфликта состояния.

const users = useGetUsersQuery();
const posts = useGetPostsQuery();

Оба запроса выполняются параллельно. RTK Query не блокирует один запрос другим и не требует ручного orchestration.

Поведение кэша при параллельных запросах

Каждый запрос записывается в общий normalized cache Redux Toolkit Query. При этом:

  • одинаковые запросы (одинаковый endpoint + аргументы) дедуплицируются
  • повторные подписки используют уже существующий запрос
  • новые подписки не инициируют повторный network call, если данные актуальны
const userA = useGetUserQuery(1);
const userB = useGetUserQuery(1);

Несмотря на два вызова, сеть будет задействована только один раз.

Одновременные запросы с разными аргументами

Частый сценарий — параллельная загрузка сущностей по разным идентификаторам.

const user = useGetUserQuery(1);
const comments = useGetCommentsQuery(1);
const posts = useGetUserPostsQuery(1);

Каждый endpoint изолирован, и RTK Query обрабатывает их независимо. При этом система автоматически управляет:

  • состоянием загрузки каждого запроса
  • ошибками каждого запроса
  • обновлением кэша отдельно

Параллельные запросы внутри одного endpoint

RTK Query позволяет запускать один и тот же endpoint несколько раз с разными параметрами.

const user1 = useGetUserQuery(1);
const user2 = useGetUserQuery(2);
const user3 = useGetUserQuery(3);

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

Использование skip для управления параллельностью

Параллельные запросы часто зависят от условий. RTK Query предоставляет механизм skip, позволяющий контролировать запуск запроса без разрушения структуры хуков.

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

const { data: posts } = useGetPostsQuery(user?.id, {
  skip: !user?.id
});

Здесь второй запрос фактически становится условно параллельным: он не запускается до получения результата первого.

skipToken как более строгая альтернатива

skipToken позволяет явно указать, что запрос не должен выполняться.

import { skipToken } from '@reduxjs/toolkit/query';

const userId = user?.id;

const { data: posts } = useGetPostsQuery(userId ?? skipToken);

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

Параллельные запросы с Promise.all через initiate

Хотя RTK Query ориентирован на React hooks, существует возможность ручного запуска запросов через dispatch.

const promises = [
  dispatch(api.endpoints.getUser.initiate(1)),
  dispatch(api.endpoints.getPosts.initiate()),
  dispatch(api.endpoints.getComments.initiate())
];

const [user, posts, comments] = await Promise.all(promises);

Такой подход полезен в сценариях:

  • серверного пререндеринга
  • загрузки данных до рендера
  • сложных сценариев orchestration вне React

RTK Query при этом сохраняет кэш и дедупликацию.

Дедупликация параллельных запросов

При одновременном запуске одинаковых запросов RTK Query объединяет их в один сетевой вызов.

const a = useGetUserQuery(1);
const b = useGetUserQuery(1);
const c = useGetUserQuery(1);

Фактически:

  • выполняется один HTTP запрос
  • создаются три подписки
  • данные синхронизируются между всеми подписчиками

Это критически важно для параллельных UI-компонентов, обращающихся к одним данным.

Параллельные запросы в разных компонентах

RTK Query работает на уровне store, поэтому запросы в разных местах приложения автоматически синхронизируются.

// Component A
useGetUserQuery(1);

// Component B
useGetUserQuery(1);

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

selectFromResult для оптимизации параллельных подписок

При большом количестве параллельных запросов важно избегать лишних ререндеров.

const { name } = useGetUserQuery(1, {
  selectFromResult: ({ data }) => ({
    name: data?.name
  })
});

Это позволяет:

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

При параллельных запросах это особенно важно, так как каждый endpoint может обновляться независимо.

Параллельные запросы разных типов данных

Типичный реальный сценарий — загрузка нескольких доменных сущностей:

const user = useGetUserQuery(1);
const notifications = useGetNotificationsQuery(1);
const settings = useGetSettingsQuery(1);
const permissions = useGetPermissionsQuery(1);

RTK Query выполняет все запросы одновременно, при этом:

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

Поведение при частичном завершении запросов

В параллельных запросах данные приходят асинхронно. Это приводит к постепенному заполнению UI:

  • один запрос может завершиться быстрее
  • другие остаются в состоянии isLoading
  • готовые данные доступны сразу
const { data: user, isLoading: userLoading } = useGetUserQuery(1);
const { data: posts, isLoading: postsLoading } = useGetPostsQuery(1);

RTK Query не требует объединения состояний вручную, но позволяет это делать при необходимости.

Комбинация параллельных и зависимых запросов

Часто используется гибридный подход:

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

const { data: posts } = useGetPostsQuery(user?.id, {
  skip: !user
});

const { data: comments } = useGetCommentsQuery(user?.id, {
  skip: !user
});

Здесь:

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

Параллельные запросы через кастомные хуки

При масштабировании API часто создаются обертки:

export const useDashboardData = (userId) => {
  const user = useGetUserQuery(userId);
  const posts = useGetUserPostsQuery(userId);
  const stats = useGetUserStatsQuery(userId);

  return {
    user,
    posts,
    stats
  };
};

Такой паттерн позволяет:

  • централизовать параллельную загрузку
  • переиспользовать группы запросов
  • контролировать единый слой данных

Инвалидация и параллельные обновления

При изменении данных RTK Query может одновременно обновлять несколько кэшей:

updateUser: builder.mutation({
  query: (data) => ({
    url: `/user/${data.id}`,
    method: 'PUT',
    body: data
  }),
  invalidatesTags: ['User', 'Posts']
});

После мутации:

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

Параллельные polling-запросы

RTK Query поддерживает polling, и несколько polling-запросов могут работать параллельно:

useGetNotificationsQuery(undefined, {
  pollingInterval: 5000
});

useGetMessagesQuery(undefined, {
  pollingInterval: 3000
});

Каждый endpoint имеет собственный цикл обновления, не блокируя другие.

Влияние параллельности на производительность

Параллельные запросы не означают неконтролируемую нагрузку. RTK Query управляет:

  • дедупликацией сетевых вызовов
  • кэшированием результатов
  • автоматическим refetch только при необходимости
  • отменой неактуальных запросов при размонтировании

Однако архитектурно важно избегать избыточного количества одновременных endpoint’ов без необходимости.

Распространённые паттерны параллельных запросов

Dashboard загрузка:

const user = useGetUserQuery(id);
const analytics = useGetAnalyticsQuery(id);
const activity = useGetActivityQuery(id);

Страница профиля:

const profile = useGetProfileQuery(id);
const friends = useGetFriendsQuery(id);
const photos = useGetPhotosQuery(id);

Глобальные данные приложения:

const config = useGetConfigQuery();
const session = useGetSessionQuery();
const notifications = useGetNotificationsQuery();

Ошибки при организации параллельных запросов

Распространённые проблемы:

  • дублирование одинаковых endpoint’ов с разными аргументами без необходимости
  • отсутствие skip при зависимых данных
  • попытка вручную синхронизировать состояния загрузки
  • чрезмерное разбиение данных на мелкие запросы

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

Поведение при повторном монтировании компонентов

При размонтировании компонента подписка на данные может сохраняться в кэше в течение времени keepUnusedDataFor.

const api = createApi({
  keepUnusedDataFor: 60
});

Это влияет на параллельные запросы следующим образом:

  • повторное появление компонента не вызывает новый запрос
  • несколько компонентов могут использовать общий кэш
  • параллельные запросы становятся фактически синхронизированными через store

Параллельные запросы и SSR

При серверном рендеринге параллельные запросы обычно выполняются через Promise.all на уровне store:

await Promise.all([
  store.dispatch(api.endpoints.getUser.initiate(1)),
  store.dispatch(api.endpoints.getPosts.initiate(1))
]);

RTK Query заполняет кэш до рендера, сохраняя структуру параллельных данных для клиента.

Итоговая модель поведения параллельных запросов

RTK Query рассматривает параллельность как совокупность независимых потоков данных:

  • каждый endpoint изолирован
  • каждый аргумент формирует отдельный кэш-ключ
  • сеть используется минимально благодаря дедупликации
  • подписки могут существовать одновременно без конфликтов
  • управление зависимостями осуществляется через skip и skipToken
  • orchestration возможна как внутри React, так и вне его