React

Хук use() и Suspense для данных

Десять лет загрузка данных в React выглядела одинаково: объявить три useState (data, loading, error), написать useEffect с fetch, не забыть про cleanup и про гонки запросов. Двадцать строк ритуала вокруг одной строки 'покажи имя'. React 19 (декабрь 2024) добавил хук use(). Теперь компонент может прочитать промис прямо в теле рендера, а пока промис не готов - просто приостановиться. Ритуал из двадцати строк сжимается до одной: const user = use(promise).

  • React 19 (декабрь 2024): use() входит в стабильный релиз как штатный способ читать промисы и контекст в рендере
  • Next.js App Router: серверный компонент создаёт промис и отдаёт его клиентскому, который дочитывает данные через use()
  • React Docs: официальная документация выделяет use() отдельной страницей и показывает связку с Suspense
  • TanStack и Relay: экосистема выстраивает интеграции вокруг чтения промисов в рендере вместо useEffect-загрузки
  • Vercel templates: стартеры демонстрируют 'промис с сервера + use() на клиенте' как канонический паттерн стриминга данных

Предварительные знания

  • Suspense и lazy: как React ловит приостановку компонента и показывает fallback
  • Паттерны загрузки данных: waterfall, параллельная загрузка, render-as-you-fetch
  • Промисы: состояния pending, fulfilled, rejected и метод then

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

До use() существовал неофициальный приём: бросить промис прямо из рендера, чтобы Suspense его поймал. Приём работал, но был хрупким и нигде не задокументированным как публичный API. Команда React хотела дать законный способ читать асинхронный ресурс прямо в рендере и заодно обойти жёсткое правило хуков - использовать загрузку условно. Так в React 19 появился use(). В отличие от прочих хуков, его можно вызывать внутри if и циклов, потому что он не хранит состояние между рендерами, а лишь читает переданный ресурс. Если ресурс - неготовый промис, компонент приостанавливается; если контекст, use() возвращает его значение.

Что читает use(): промис и контекст

use() принимает ресурс и возвращает его значение. Ресурсов два вида. Первый - промис: use() как бы 'разворачивает' его. Если промис уже разрешён, возвращается результат. Если ещё в полёте, компонент приостанавливается, и ближайший Suspense показывает fallback. Если промис отклонён, ошибка летит к ближайшему error boundary. Второй вид - контекст: use(MyContext) возвращает текущее значение, как useContext, но вызывать его можно условно.

Ключевая деталь: компонент написан так, будто данные уже есть. Нет ветки 'если loading', нет проверки 'если error'. Эти исходы вынесены наружу - в Suspense и error boundary. Компонент описывает только успешный случай, а граничные состояния обрабатывает дерево вокруг него. Это и делает код с use() короче ручной загрузки.

Обычные хуки (useState, useEffect, useContext) нельзя вызывать условно - порядок вызовов между рендерами должен быть постоянным. use() - исключение: он не хранит состояние между рендерами, поэтому его можно ставить внутри if или цикла. Но это единственный хук с таким послаблением, на остальные правило хуков по-прежнему распространяется.

Что произойдёт, когда use(promise) встречает промис в состоянии pending?

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

Самая частая ошибка с use() - создать промис прямо в теле компонента. Рендер в React может повторяться много раз. Если на каждом рендере вызывать fetch заново, получится новый промис каждый раз. use() видит неготовый промис, приостанавливает компонент, тот рендерится снова, создаёт ещё один промис, и так по кругу. Бесконечный цикл загрузки.

Промис должен быть стабильным между рендерами. Откуда брать стабильный промис: создать его выше по дереву и передать пропом; вернуть из серверного компонента; держать в кэше, который при одинаковом ключе отдаёт тот же самый промис. Идея ровно та же, что и в render-as-you-fetch: запрос стартует один раз, до и вне рендера, а use() лишь дочитывает его результат.

Самый чистый источник стабильного промиса - серверный компонент: он создаёт промис на сервере один раз и передаёт его клиентскому компоненту пропом. В чисто клиентском коде роль кэша берут на себя библиотеки данных (TanStack Query) или роутеры с loader'ами, чтобы не городить Map руками.

Почему вызов use(fetch(...).then(...)) прямо в теле компонента приводит к бесконечному циклу?

use() против загрузки в useEffect

Загрузка в useEffect и чтение через use() решают одну задачу разными моделями. В useEffect-подходе компонент сначала рендерится без данных, затем эффект запускает запрос, ответ кладётся в useState, и происходит второй рендер. Разработчик вручную ведёт три состояния и следит за гонками. С use() данные читаются прямо в рендере, а loading и error вынесены в Suspense и error boundary.

  • Загрузка в useEffect — Три useState (data, loading, error), эффект с fetch, cleanup и защита от гонок. Это fetch-on-render: запрос стартует после первого рендера. Много кода, легко допустить waterfall.
  • Чтение через use() — Стабильный промис создан заранее (на сервере или в кэше), компонент читает его одной строкой. loading и error вынесены в Suspense и error boundary. Это render-as-you-fetch.
АспектuseEffect + fetchuse() + Suspense
Где стартует запросПосле рендера (fetch-on-render)До рендера, стабильный промис
loading-состояниеРучной флаг в useStatefallback у Suspense
Обработка ошибкиРучной флаг + try/catchБлижайший error boundary
Гонки запросовРешать вручную в cleanupСнимаются стабильным промисом

use() не отменяет useEffect целиком. Эффекты по-прежнему нужны для подписок, таймеров, синхронизации с не-React системами. use() заменяет именно паттерн ручной загрузки данных в эффекте. А кэширование, инвалидацию и фоновое обновление в клиентском коде по-прежнему удобнее доверить TanStack Query, чем писать Map руками.

Что use() + Suspense забирает у разработчика по сравнению с ручной загрузкой в useEffect?

Связь с другими темами

use() - мост между промисами, Suspense и серверными данными. Соседние темы:

  • Suspense и lazy — Механизм, который ловит приостановку от use() и показывает запасной UI
  • Server Components — Сервер создаёт промис и стримит его клиенту, где use() дочитывает результат
  • Паттерны загрузки данных — use() механически реализует render-as-you-fetch: промис стартует до рендера, читается в рендере

Итог

  • use() читает ресурс прямо в рендере: для промиса возвращает его значение или приостанавливает компонент, для контекста возвращает текущее значение
  • use() работает в паре с Suspense (loading) и error boundary (ошибки): приостановку и reject ловят именно они, а не сам хук
  • Промис нельзя создавать прямо в рендере: каждый рендер давал бы новый промис и бесконечный цикл; промис должен быть стабильным и кэшированным
  • Источник стабильного промиса - серверный компонент, кэш данных или память вне компонента, а не вызов fetch в теле функции
  • В отличие от других хуков, use() можно вызывать условно (в if, в цикле), потому что он не хранит состояние между рендерами

Связанные уроки

  • rc-30-rsc-intro — Server Components часто создают промис на сервере и передают его клиенту, где use() его дочитывает - прямое применение хука
  • rc-21-lazy-suspense — use() приостанавливает компонент, а ловит эту приостановку и показывает fallback именно Suspense
  • rc-28-data-fetching-patterns — use() - механическая реализация render-as-you-fetch, разобранного в паттернах загрузки
Хук use() и Suspense для данных

0

1

Войти