Dependent queries

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

TanStack Query предоставляет несколько механизмов для построения таких связей, сохраняя при этом декларативность, кэширование и контроль над состояниями загрузки.


Базовая идея зависимых запросов

Зависимый запрос — это запрос, который не выполняется до тех пор, пока не будут доступны данные из другого запроса.

Ключевая особенность заключается в управлении enabled:

useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  enabled: !!userId
})

В этом примере запрос выполняется только тогда, когда userId существует. Это самый простой вариант зависимости, основанный на наличии входного параметра.


Одноуровневая зависимость запросов

Типичный сценарий: сначала загружается пользователь, затем его профиль расширенного типа.

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

const userId = user?.id

const { data: profile } = useQuery({
  queryKey: ['profile', userId],
  queryFn: () => fetchProfile(userId),
  enabled: !!userId
})

Здесь второй запрос зависит от результата первого. Основные особенности:

  • profile не запрашивается до появления user.id
  • ключ запроса включает зависимое значение
  • кэширование разделяется по userId

Важность корректного queryKey при зависимостях

Ключ запроса определяет кэш, дедупликацию и повторное использование данных. Ошибки в структуре ключа приводят к:

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

Правильный подход:

queryKey: ['profile', userId]

Неправильный подход:

queryKey: ['profile']

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


Несколько зависимых уровней

Сценарий цепочки:

  1. загрузка пользователя
  2. загрузка его организации
  3. загрузка настроек организации
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser
})

const userId = user?.id

const { data: organization } = useQuery({
  queryKey: ['organization', userId],
  queryFn: () => fetchOrganization(userId),
  enabled: !!userId
})

const orgId = organization?.id

const { data: settings } = useQuery({
  queryKey: ['settings', orgId],
  queryFn: () => fetchSettings(orgId),
  enabled: !!orgId
})

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


Динамическое управление enabled

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

enabled: !!userId && user.status === 'active'

Или комбинацией нескольких факторов:

enabled: !!userId && !!organizationId && hasAccess

Важно учитывать, что enabled пересчитывается при каждом рендере, поэтому он должен оставаться чистым выражением без побочных эффектов.


Зависимость от нескольких запросов

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

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

const { data: permissions } = useQuery({
  queryKey: ['permissions'],
  queryFn: fetchPermissions
})

const userId = user?.id
const canLoadDashboard = permissions?.includes('dashboard')

const { data: dashboard } = useQuery({
  queryKey: ['dashboard', userId],
  queryFn: () => fetchDashboard(userId),
  enabled: !!userId && canLoadDashboard
})

Здесь выполнение зависит сразу от двух независимых источников.


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

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

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

const { data: profile } = useQuery({
  queryKey: ['profile', userId],
  queryFn: () => fetchProfile(userId),
  enabled: !!userId
})

Преимущество подхода:

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

Синхронизация зависимых запросов через stale data

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

enabled: !!userId

При этом:

  • если userId уже известен из кэша, запрос не задерживается
  • если данные устарели, может произойти фоновый рефетч
  • система автоматически поддерживает консистентность

Предотвращение лишних запросов

Зависимые запросы могут привести к каскадным перезапросам при каждом изменении промежуточного значения.

Типичная ошибка:

enabled: !!user?.id

Если user пересоздаётся на каждом рендере, возможны лишние активации.

Более стабильный вариант:

const userId = useMemo(() => user?.id, [user?.id])

enabled: !!userId

Зависимые мутации и последующие запросы

Хотя зависимые запросы чаще связываются с useQuery, на практике они часто сочетаются с мутациями.

const mutation = useMutation({
  mutationFn: createOrder,
  onSuccess: (order) => {
    queryClient.invalidateQueries({
      queryKey: ['orders', order.userId]
    })
  }
})

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


Параллельные зависимости и условные ветвления

Иногда структура зависимостей становится ветвящейся:

if (user?.role === 'admin') {
  // загружаются админ-данные
}

В TanStack Query это выражается через условные enabled:

const isAdmin = user?.role === 'admin'

const { data: adminData } = useQuery({
  queryKey: ['admin-data', user?.id],
  queryFn: fetchAdminData,
  enabled: !!user?.id && isAdmin
})

Глубокие цепочки и их стоимость

Чем глубже цепочка зависимостей, тем выше вероятность:

  • увеличения времени полной загрузки экрана
  • накопления промежуточных состояний loading
  • усложнения отладки причин повторных запросов

Типичная проблема:

user → organization → projects → tasks → comments

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


Оптимизация цепочек зависимостей

Одним из решений является частичное распараллеливание:

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

const userId = user?.id

const { data: organization } = useQuery({
  queryKey: ['organization', userId],
  queryFn: () => fetchOrganization(userId),
  enabled: !!userId
})

const { data: projects } = useQuery({
  queryKey: ['projects', userId],
  queryFn: () => fetchProjects(userId),
  enabled: !!userId
})

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


Комбинированный подход с useQueries

Для множественных зависимых запросов применяется useQueries:

const results = useQueries({
  queries: [
    {
      queryKey: ['profile', userId],
      queryFn: () => fetchProfile(userId),
      enabled: !!userId
    },
    {
      queryKey: ['settings', userId],
      queryFn: () => fetchSettings(userId),
      enabled: !!userId
    }
  ]
})

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


Зависимые запросы и кеширование

Кэш играет ключевую роль в производительности цепочек зависимостей:

  • повторный переход по цепочке использует ранее загруженные данные
  • staleTime снижает частоту повторных запросов
  • gcTime определяет время жизни зависимых данных

Корректная настройка кэша уменьшает влияние глубокой зависимости на UX.


Типичные ошибки в dependent queries

На практике чаще всего встречаются следующие проблемы:

  • отсутствие ключа зависимого параметра в queryKey
  • использование enabled без проверки всех зависимостей
  • попытка вычислить зависимость внутри queryFn
  • чрезмерная вложенность цепочек
  • игнорирование кэширования при повторных запросах

Каждая из этих ошибок приводит либо к лишним запросам, либо к неконсистентным данным.


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

Внутри TanStack Query зависимость реализуется не как явный граф, а как реактивная система условий:

  • изменение состояния приводит к пересчёту enabled
  • включение запроса запускает fetch
  • результат сохраняется в cache
  • зависимые запросы получают новые данные через re-render

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