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

Параллельные запросы — один из базовых сценариев работы с TanStack Query, при котором несколько независимых запросов выполняются одновременно, не блокируя друг друга и не создавая цепочек ожидания. Такая модель позволяет эффективно загружать данные из разных источников, минимизировать время рендера и избегать классической проблемы waterfall-запросов, характерной для последовательной загрузки данных в React-приложениях.

Самый прямой способ организации параллельной загрузки данных — использование нескольких useQuery в одном компоненте. Каждый запрос существует как отдельная единица состояния внутри QueryClient и обрабатывается независимо.

import { useQuery } from '@tanstack/react-query'

function Dashboard() {
  const usersQuery = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  const postsQuery = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  const commentsQuery = useQuery({
    queryKey: ['comments'],
    queryFn: fetchComments,
  })

  return {
    usersQuery,
    postsQuery,
    commentsQuery,
  }
}

Каждый из этих запросов запускается одновременно при монтировании компонента. TanStack Query не связывает их выполнение, если не задана явная зависимость через enabled.

Ключевой момент заключается в том, что параллельность здесь возникает естественным образом: React инициирует рендер, TanStack Query регистрирует запросы, и QueryClient запускает их независимо.

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

Каждый запрос идентифицируется через queryKey. Именно он определяет, как данные будут кэшироваться и переиспользоваться.

queryKey: ['users']
queryKey: ['posts']
queryKey: ['comments']

Даже при одновременном запуске TanStack Query гарантирует:

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

Если два компонента используют один и тот же queryKey, запрос не выполняется дважды — данные берутся из общего кэша.

useQueries для динамических параллельных запросов

Когда количество запросов заранее неизвестно или формируется динамически, используется useQueries. Это основной инструмент для масштабируемых параллельных операций.

import { useQueries } from '@tanstack/react-query'

function UsersList({ userIds }) {
  const queries = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUserById(id),
    })),
  })

  return queries
}

Каждый элемент массива описывает отдельный запрос, а TanStack Query выполняет их параллельно.

Особенность useQueries заключается в том, что он возвращает массив результатов в том же порядке, в котором были переданы конфигурации. Это упрощает сопоставление данных с исходными идентификаторами.

Управление загрузкой в параллельных запросах

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

const isLoading = usersQuery.isLoading || postsQuery.isLoading
const isError = usersQuery.isError || postsQuery.isError

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

const queries = [usersQuery, postsQuery, commentsQuery]

const isLoading = queries.some(q => q.isLoading)
const isSuccess = queries.every(q => q.isSuccess)
const isError = queries.some(q => q.isError)

Такая модель особенно полезна в dashboard-интерфейсах, где несколько независимых блоков данных должны отображаться одновременно.

Снижение waterfall-эффекта

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

Пример плохой архитектуры:

const users = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

const posts = useQuery({
  queryKey: ['posts', users.data?.id],
  queryFn: () => fetchPosts(users.data.id),
  enabled: !!users.data,
})

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

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

const users = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

const posts = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

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

Параллельные запросы и React рендеринг

Каждый useQuery вызывает отдельный ререндер при изменении своего состояния. В случае параллельных запросов это может приводить к нескольким последовательным обновлениям UI.

TanStack Query минимизирует этот эффект за счёт:

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

Тем не менее, при большом количестве запросов полезно разделять компоненты, чтобы локализовать ререндеринг.

Оптимизация через staleTime и cacheTime

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

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 5,
})

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

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

Ошибки и их агрегация

При параллельных запросах ошибки не блокируют выполнение остальных запросов. Это важное отличие от последовательных цепочек.

if (usersQuery.isError) {
  // обработка ошибки пользователей
}

if (postsQuery.isError) {
  // обработка ошибки постов
}

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

const errors = [usersQuery, postsQuery, commentsQuery]
  .filter(q => q.isError)

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

Параллельные запросы в useQueries и динамические зависимости

Наиболее сложные сценарии возникают, когда часть запросов зависит от внешних параметров, но их количество остаётся динамическим.

const queries = useQueries({
  queries: projects.map(project => ({
    queryKey: ['project', project.id, 'stats'],
    queryFn: () => fetchProjectStats(project.id),
    enabled: !!project.id,
  })),
})

Здесь TanStack Query управляет каждым запросом отдельно, даже если часть из них временно отключена через enabled.

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

Параллельные запросы в TanStack Query устойчивы к повторным рендерам компонента. Это достигается за счёт:

  • стабилизации queryKey
  • хранения состояния в QueryCache
  • дедупликации запросов

Даже если компонент перерендерится несколько раз, повторный сетевой запрос не выполняется, если данные уже находятся в состоянии fresh или stale в кэше и не истёк срок их актуальности.

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

При серверном рендеринге параллельные запросы выполняются на сервере одновременно, что позволяет сократить время подготовки HTML.

await Promise.all([
  queryClient.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers }),
  queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts }),
])

После гидратации клиент получает уже заполненный кэш и не инициирует повторные запросы, если данные не устарели.

Смешанные сценарии: параллельные и частично зависимые запросы

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

const user = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
})

const userSettings = useQuery({
  queryKey: ['userSettings', user.data?.id],
  queryFn: () => fetchUserSettings(user.data.id),
  enabled: !!user.data,
})

const posts = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

Здесь user и posts выполняются параллельно, а userSettings зависит от результата первого запроса.

Такая комбинация позволяет строить эффективные загрузочные стратегии без полного отказа от параллелизма.

Масштабирование параллельных запросов

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

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

Иногда выгоднее агрегировать API-ответы на сервере, чем запускать десятки отдельных запросов на клиенте. TanStack Query не ограничивает архитектуру, но требует осознанного разделения ответственности между клиентом и сервером.

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