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 + fetch | use() + Suspense |
|---|---|---|
| Где стартует запрос | После рендера (fetch-on-render) | До рендера, стабильный промис |
| loading-состояние | Ручной флаг в useState | fallback у 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, разобранного в паттернах загрузки