Menu

Хук use в React: промисы и контекст во время рендера

use читает значение промиса или контекста во время рендера. С промисом он приостанавливает компонент, пока не придут данные, и показывает ближайший запасной вариант Suspense. В отличие от других хуков, его можно вызывать внутри if и циклов.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

use это API React 19, который читает значение из промиса или контекста во время рендера компонента. use(promise) приостанавливает компонент, пока промис не выполнится, а тем временем показывает ближайший запасной вариант <Suspense>; use(context) работает как useContext, но может стоять внутри if или цикла.

Функция fetchUser ниже заменяет настоящий запрос.

Первый рендер на секунду показывает «Loading...», затем профиль. Нажмите User 2: запасной вариант возвращается, пока новый промис ожидает. У Profile нет собственного состояния загрузки; он написан так, будто данные уже есть.

Как работает use(promise)

Когда Profile вызывает use(userPromise):

  • Если промис выполнен, use возвращает его значение, и рендер продолжается.
  • Если он ещё ожидает, React прекращает рендер Profile и показывает запасной вариант ближайшего <Suspense> выше. Когда промис завершится, React снова рендерит Profile, и на этот раз use возвращает значение.
  • Если он отклонён, React вместо этого показывает ближайшую границу ошибок.

React запоминает результат в самом объекте промиса. Поэтому при следующем рендере промис должен быть тем же объектом.

Чтобы старый профиль оставался на экране, пока загружается следующий, устанавливайте промис внутри перехода. Тогда React ждёт, а не показывает запасной вариант снова:

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

Где создавать промис

На этом спотыкаются чаще всего. Вызов fetch внутри компонента, который его читает, выглядит естественно и не работает:

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

Каждый рендер вызывает fetchUser и получает новый ожидающий промис. Profile приостанавливается. Когда этот промис выполняется, React снова рендерит Profile, который создаёт ещё один ожидающий промис, который снова приостанавливает компонент. Компонент так и не показывает данные, а вкладка сети заполняется запросами.

Создавайте промис там, где он не пересоздаётся при каждом рендере читающего компонента:

  • В родителе: в состоянии (как в первом примере) или в обработчике события.
  • На уровне модуля, когда данные не зависят от пропсов: const configPromise = fetchConfig(); над компонентом.
  • В кэше по ключу аргументов, чтобы для одного и того же id возвращался один и тот же промис.
  • Во фреймворке: серверный компонент может передать промис клиентскому компоненту как пропс, и загрузчики маршрутов делают то же. Именно для этого случая use и создавался.

Вот версия с кэшем, где компонент может запрашивать данные по id и всё равно получать стабильный промис:

Пройдитесь по трём городам, а затем вернитесь к уже открытому. Консоль выводит по одному запросу на город, а город, который вы уже видели, появляется без запасного варианта, потому что его промис уже выполнен. Удалите проверку if (!cache.has(city)), и компонент снова будет приостанавливаться бесконечно.

Настоящему кэшу также нужен способ устаревания записей. Это берут на себя библиотеки данных и фреймворки, поэтому большинство приложений получают промисы от них.

use(context), даже внутри if

use(SomeContext) возвращает то же значение, что и useContext(SomeContext). Разница в том, где его можно вызывать. Все остальные хуки должны выполняться на верхнем уровне, в одном и том же порядке при каждом рендере (правила хуков). use можно вызывать после раннего return, внутри условия или в цикле.

Нажмите кнопку, чтобы переключаться между двумя ветками. Замените use(UserContext) на useContext(UserContext), и код нарушит правила хуков, потому что хук выполнялся бы в одних рендерах и не выполнялся бы в других.

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

Ошибки: границы ошибок, а не try/catch

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

Откройте Post 2, и граница покажет сообщение об ошибке. Снова откройте Post 1, и всё работает: key={id} даёт границе свежее состояние для каждого поста, поэтому она не продолжает показывать старую ошибку.

Перехватить ожидающий или отклонённый use через try/catch нельзя. Пока промис ожидает, use прерывает рендер, выбрасывая особое значение, и catch вокруг него проглотил бы его. Перехваченное значение это ошибка, сообщение которой начинается с "Suspense Exception: This is not a real error!", а в разработке React также выводит "use was called from inside a try/catch block". Чтобы показать запасное значение вместо экрана ошибки, обработайте отклонение на промисе, прежде чем передавать его вниз:

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

use в сравнении с загрузкой в useEffect

Классический клиентский шаблон загружает данные в эффекте и хранит три значения состояния:

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
Когда начинается запроспосле того как первый рендер оказался на экранекогда создаётся промис, возможно до рендера компонента
Состояние загрузкиваш собственный if (!user)ближайший <Suspense>
Ошибкиваше собственное состояние errorближайшая граница ошибок
Состояния гонкизащищаете вы сами (ignore)компонент всегда читает промис, который ему дали
Кто создаёт промискомпонентродитель, кэш или фреймворк

Версия с эффектом самодостаточна, поэтому она по-прежнему самый распространённый подход в приложениях без фреймворка; страница о загрузке данных разбирает её полностью. use раскрывается, когда что-то выше компонента рано запускает запрос и передаёт промис вниз.

Часто задаваемые вопросы

Что делает хук use в React?

use(resource) возвращает значение промиса или контекста. С промисом компонент приостанавливается, пока тот не выполнится, а ближайший <Suspense> показывает свой запасной вариант; если промис отклоняется, вместо этого показывается ближайшая граница ошибок.

Можно ли вызывать use по условию?

Да. use это единственный хук, который можно вызывать внутри инструкций if, циклов и после раннего return. Его всё равно нужно вызывать из компонента или хука, а не из обработчика события или эффекта.

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

Каждый рендер создаёт новый промис. Компонент приостанавливается на нём, React рендерит снова, когда тот выполнится, и этот рендер создаёт ещё один ожидающий промис. Создавайте промис вне рендера: в состоянии родителя, на уровне модуля, в кэше или в загрузчике фреймворка.

Можно ли обернуть use в try/catch?

Нет. Когда промис ожидает, use прерывает рендер, выбрасывая значение, и перехват его ломает. Отклонённый промис обрабатывайте границей ошибок или добавьте к промису .catch(), чтобы он разрешался в запасное значение.

Заменить загрузку данных в useEffect на use?

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

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ