Параллельные запросы — один из базовых сценариев работы с 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. Это основной
инструмент для масштабируемых параллельных операций.
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-интерфейсах, где несколько независимых блоков данных должны отображаться одновременно.
Параллельные запросы решают проблему последовательной загрузки, когда один запрос зависит от завершения другого без необходимости.
Пример плохой архитектуры:
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,
})
Это снижает общее время ожидания, поскольку оба запроса выполняются одновременно.
Каждый useQuery вызывает отдельный ререндер при
изменении своего состояния. В случае параллельных запросов это может
приводить к нескольким последовательным обновлениям UI.
TanStack Query минимизирует этот эффект за счёт:
Тем не менее, при большом количестве запросов полезно разделять компоненты, чтобы локализовать ререндеринг.
При параллельных запросах нагрузка на сеть может увеличиваться, особенно если данные часто пересоздаются при навигации между экранами.
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)
Это позволяет строить частично деградирующие интерфейсы, где часть данных может отсутствовать без полного отказа страницы.
Наиболее сложные сценарии возникают, когда часть запросов зависит от внешних параметров, но их количество остаётся динамическим.
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Даже если компонент перерендерится несколько раз, повторный сетевой
запрос не выполняется, если данные уже находятся в состоянии
fresh или stale в кэше и не истёк срок их
актуальности.
При серверном рендеринге параллельные запросы выполняются на сервере одновременно, что позволяет сократить время подготовки 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 не ограничивает архитектуру, но требует осознанного разделения ответственности между клиентом и сервером.
Параллельные запросы остаются базовым механизмом построения реактивных интерфейсов, где независимые источники данных обновляются одновременно и согласованно, без блокировки друг друга и без избыточной координации между состояниями.