Suspense для данных

Suspense-подход в TanStack Query строится вокруг идеи переноса состояния загрузки на уровень React Suspense, исключая необходимость ручной обработки isLoading, isFetching в компонентах. Данные становятся частью механизма «приостановки» рендера, а не состоянием, управляемым внутри UI-логики.

В TanStack Query Suspense интегрируется через специальные режимы запроса и обёртки React Suspense + Error Boundary.


Базовый принцип работы Suspense в React-экосистеме

React Suspense перехватывает «незавершённый» процесс получения данных и откладывает рендер компонента до момента, когда данные станут доступны.

В контексте TanStack Query это реализуется через:

  • выброс Promise при отсутствии данных в кеше
  • повторный рендер после завершения запроса
  • делегирование состояния загрузки React Suspense boundary

Включение Suspense в TanStack Query

TanStack Query предоставляет два основных способа работы с Suspense:

  • глобальная настройка QueryClient
  • локальная настройка на уровне запроса

Глобальная активация

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true
    }
  }
})

В этом режиме каждый query автоматически переходит в Suspense-модель.


Локальная активация

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

const fetchUser = async () => {
  const res = await fetch('/api/user')
  return res.json()
}

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

  return <div>{data.name}</div>
}

Suspense активируется только для конкретного запроса.


useSuspenseQuery как специализированный API

В новых версиях TanStack Query используется отдельный хук useSuspenseQuery, который убирает необходимость ручного указания suspense: true.

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

function User() {
  const { data } = useSuspenseQuery({
    queryKey: ['user'],
    queryFn: async () => {
      const res = await fetch('/api/user')
      return res.json()
    }
  })

  return <div>{data.name}</div>
}

Поведение:

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

Обёртка Suspense Boundary

Suspense-режим невозможен без React Suspense boundary.

import { Suspense } from 'react'

function App() {
  return (
    <Suspense fallback={<div>Загрузка данных...</div>}>
      <User />
    </Suspense>
  )
}

Fallback отображается до завершения всех зависимых запросов внутри дерева компонентов.


Error Boundary в связке с Suspense

Suspense не обрабатывает ошибки. Для этого используется Error Boundary.

import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error }) {
  return <div>Ошибка: {error.message}</div>
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<div>Загрузка...</div>}>
        <User />
      </Suspense>
    </ErrorBoundary>
  )
}

Поведение кеша в Suspense-режиме

TanStack Query использует кеш как основной источник синхронизации Suspense.

Ключевые особенности:

  • при наличии данных в кеше Suspense не срабатывает
  • повторные запросы используют stale-данные без блокировки UI
  • переход между страницами может не вызывать fallback

StaleTime и влияние на Suspense

staleTime определяет, считается ли кеш актуальным.

useSuspenseQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 1000 * 60
})

Поведение:

  • данные свежие → моментальный рендер без suspend
  • данные устаревшие → возможный refetch с фоновой загрузкой

Suspense при этом не блокирует отображение, если есть кеш.


Prefetch как оптимизация Suspense

Prefetch позволяет подготовить данные до входа в компонент.

queryClient.prefetchQuery({
  queryKey: ['user'],
  queryFn: fetchUser
})

Эффект:

  • Suspense boundary не активируется
  • данные уже доступны в кеше
  • UI отображается без задержек

Parallel Suspense-запросы

Несколько Suspense-запросов внутри одного дерева объединяются React-ом.

function Dashboard() {
  const user = useSuspenseQuery({ queryKey: ['user'], queryFn: fetchUser })
  const posts = useSuspenseQuery({ queryKey: ['posts'], queryFn: fetchPosts })

  return (
    <>
      <div>{user.data.name}</div>
      <div>{posts.data.length}</div>
    </>
  )
}

Поведение:

  • Suspense срабатывает до завершения всех запросов
  • fallback отображается один раз
  • повторный рендер происходит после готовности всех данных

Streaming и постепенная гидрация

Suspense в TanStack Query совместим с streaming SSR сценариями.

Особенности:

  • сервер передаёт частично готовый HTML
  • клиент «дозапрашивает» недостающие данные
  • React повторно активирует boundary при необходимости

Интеграция с SSR (Hydration)

Hydration позволяет избежать повторных запросов на клиенте.

import { dehydrate, HydrationBoundary } from '@tanstack/react-query'

export async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['user'],
    queryFn: fetchUser
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient)
    }
  }
}

Клиент:

function App({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Suspense fallback={<div>Loading...</div>}>
        <User />
      </Suspense>
    </HydrationBoundary>
  )
}

Ошибки и повторные попытки в Suspense-режиме

Поведение ошибок отличается от классического режима:

  • ошибка выбрасывается в Error Boundary
  • повторные попытки управляются через retry
  • Suspense не обрабатывает retry UI
useSuspenseQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  retry: 2
})

Отличия Suspense от стандартного режима

Классический режим

  • isLoading
  • isError
  • isFetching
  • ручное управление UI

Suspense режим

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

Ленивая загрузка через Suspense

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

const User = React.lazy(() => import('./User'))

function App() {
  return (
    <Suspense fallback={<div>Loading route...</div>}>
      <User />
    </Suspense>
  )
}

В сочетании с TanStack Query данные загружаются синхронно с компонентом.


Влияние refetchOnMount и refetchOnWindowFocus

В Suspense-режиме фоновые обновления продолжают работать.

useSuspenseQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  refetchOnWindowFocus: true
})

Поведение:

  • UI уже отображён
  • данные обновляются асинхронно
  • Suspense не активируется повторно

Композиция Suspense-запросов

Suspense-запросы можно комбинировать в отдельные слои данных.

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

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


Типичные архитектурные паттерны

Глобальный Suspense boundary

Один boundary на всё приложение приводит к централизованной загрузке.

Локальные boundary

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

Гибридная модель

Комбинация глобального fallback и локальных fallback-узлов для частичных загрузок.


Оптимизация поведения Suspense

Ключевые механизмы оптимизации:

  • prefetch перед навигацией
  • использование staleTime для минимизации suspend
  • разделение queryKey по уровням данных
  • контроль refetch поведения

Ограничения Suspense в TanStack Query

  • сложность дебага цепочек suspend
  • необходимость Error Boundary
  • потенциальная избыточность при мелких запросах
  • чувствительность к архитектуре кеша

Поведение при конкурентных запросах

При одновременных запросах с одинаковым queryKey:

  • используется единый Promise
  • Suspense блокирует только один раз
  • кеш разделяется между подписчиками

Итеративное обновление данных

Suspense не блокирует последующие обновления:

  • initial render через cache
  • background refetch
  • автоматическая синхронизация UI

Модель превращается в поток данных, а не статический запрос.