Перейти к содержанию

use

use - это хук React, который позволяет вам прочитать значение ресурса, например промиса или контекста.

1
const value = use(resource);

Описание

use(resource)

Вызовите use в вашем компоненте, чтобы прочитать значение ресурса, например Promise или context.

1
2
3
4
5
6
7
import { use } from 'react';

function MessageComponent({ messagePromise }) {
    const message = use(messagePromise);
    const theme = use(ThemeContext);
    // ...
}

В отличие от всех остальных хуков React, use можно вызывать в циклах и условных операторах, таких как if. Как и другие хуки React, функция, вызывающая use, должна быть компонентом или хуком.

При вызове с Promise хук use интегрируется с Suspense и error boundaries. Компонент, вызывающий use, приостанавливается на время выполнения Promise, переданного use. Если компонент, вызывающий use, обернут в границу Suspense, то будет отображен fallback. После разрешения Promise, Suspense fallback заменяется рендерингом компонентов, использующих данные, возвращенные хуком use. Если промис, переданный в use, отклонен, будет отображен фаллбек ближайшей границы ошибки.

Параметры

  • resource: это источник данных, из которого вы хотите прочитать значение. Ресурсом может быть промис или контекст.

Возвращает

Хук use возвращает значение, которое было прочитано из ресурса, как разрешенное значение промиса или контекст.

Замечания

  • Хук use должен быть вызван внутри компонента или хука.
  • При получении данных в серверном компоненте отдавайте предпочтение async и await, а не use. async и await выполняют рендеринг с того момента, когда был вызван await, в то время как use повторно рендерит компонент после разрешения данных.
  • Предпочтительнее создавать промисы в Компонентах сервера и передавать их в Компоненты клиента, чем создавать обещания в компонентах клиента. Обещания, созданные в клиентских компонентах, пересоздаются при каждом рендере. Обещания, переданные из серверного компонента в клиентский компонент, стабильны при каждом рендере. См. этот пример.

use(context)

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

1
2
3
4
5
import { use } from 'react';

function Button() {
  const theme = use(ThemeContext);
  // ...

Смотрите другие примеры ниже.

Параметры

Возвращаемое значение

Значение контекста для переданного контекста. Его определяет ближайший провайдер контекста выше компонента, который вызвал use. Если провайдера нет, возвращается defaultValue, переданный в createContext.

Предупреждения

  • use нужно вызывать внутри компонента или хука.
  • Чтение контекста через use не поддерживается в серверных компонентах.

use(promise)

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

1
2
3
4
5
import { use } from 'react';

function MessageComponent({ messagePromise }) {
  const message = use(messagePromise);
  // ...

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

Смотрите другие примеры ниже.

Параметры

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

Возвращаемое значение

Разрешённое значение промиса.

Предупреждения

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

use(browser())

Вызовите use со значением, которое вернул browser, в компоненте, который должен рендериться только в браузере:

1
2
3
4
5
6
7
import { use } from 'react';
import { browser } from 'react-dom';

function BrowserOnly() {
  use(browser('This component requires browser APIs.'));
  return <BrowserContent />;
}

Во время серверного рендеринга компонент, вызывающий use(browser()), приостанавливается, и React включает в HTML фолбэк ближайшей границы <Suspense>. В браузере use(browser()) возвращает undefined, и компонент рендерится как обычно.

Смотрите пример ниже.

Параметры

  • browserValue: значение, которое вернул browser.

Возвращаемое значение

В браузере use(browser()) возвращает undefined.

Предупреждения

  • Компонент, вызывающий use(browser()), во время серверного рендеринга должен быть внутри границы <Suspense>. Без неё серверный рендеринг завершится ошибкой.
  • В приложении с React Server Components use(browser()) нужно вызывать из клиентского компонента, а не из серверного компонента.

Использование

Использование (контекст)

Чтение контекста с помощью use

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

1
2
3
4
5
6
import { use } from 'react';

function Button() {
    const theme = use(ThemeContext);
    // ...
}

use возвращает значение контекста для переданного вами контекста. Чтобы определить значение контекста, React просматривает дерево компонентов и находит ближайший провайдер контекста выше для данного контекста.

Чтобы передать контекст кнопке Button, оберните ее или один из ее родительских компонентов в соответствующий провайдер контекста.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function MyPage() {
    return (
        <ThemeContext.Provider value="dark">
            <Form />
        </ThemeContext.Provider>
    );
}

function Form() {
    // ... renders buttons inside ...
}

Не имеет значения, сколько слоев компонентов находится между провайдером и Button. Когда Button в любом месте внутри Form вызывает use(ThemeContext), она получит "dark" в качестве значения.

В отличие от useContext, use можно вызывать в условиях и циклах, как if.

1
2
3
4
5
6
7
function HorizontalRule({ show }) {
    if (show) {
        const theme = use(ThemeContext);
        return <hr className={theme} />;
    }
    return false;
}

use вызывается внутри оператора if, позволяя вам условно считывать значения из Context.

Ближайший провайдер

Как и useContext, use(context) всегда ищет ближайшего провайдера контекста выше компонента, который его вызывает. Он ищет вверх и не рассматривает провайдеров контекста в компоненте, из которого вы вызываете use(context).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { createContext, use } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
    return (
        <ThemeContext.Provider value="dark">
            <Form />
        </ThemeContext.Provider>
    );
}

function Form() {
    return (
        <Panel title="Welcome">
            <Button show={true}>Sign up</Button>
            <Button show={false}>Log in</Button>
        </Panel>
    );
}

function Panel({ title, children }) {
    const theme = use(ThemeContext);
    const className = 'panel-' + theme;
    return (
        <section className={className}>
            <h1>{title}</h1>
            {children}
        </section>
    );
}

function Button({ show, children }) {
    if (show) {
        const theme = use(ThemeContext);
        const className = 'button-' + theme;
        return (
            <button className={className}>
                {children}
            </button>
        );
    }
    return false;
}

Чтение промиса из контекста

Чтобы делиться асинхронными данными без прокидывания пропсов, положите промис в значение контекста, прочитайте его через use(context) и разрешите через use(promise):

1
2
3
4
5
6
7
8
import { use } from 'react';
import { UserContext } from './UserContext';

function Profile() {
  const userPromise = use(UserContext);
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

Чтобы прочитать значение, нужны два вызова use, потому что само значение контекста не ожидается. Прежде чем тянуться к контексту, смотрите альтернативы в разделе Прежде чем использовать контекст.

Оберните компоненты, которые читают промис, в границу Suspense, чтобы приостановилось только это поддерево, пока промис ожидает. Подробнее о чтении промисов через use — в разделе Использование (промисы) ниже.

Подводный камень

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

Использование (промисы)

Чтение промиса с помощью use

Вызовите use с промисом, чтобы прочитать его разрешённое значение. Пока промис ожидает, компонент приостановится.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { use } from 'react';

function Albums({ albumsPromise }) {
  const albums = use(albumsPromise);
  return (
    <ul>
      {albums.map(album => (
        <li key={album.id}>
          {album.title} ({album.year})
        </li>
      ))}
    </ul>
  );
}

Оберните компонент, который вызывает use, в границу Suspense, чтобы React мог показать фолбэк, пока промис ожидает. Ближайшая граница Suspense выше приостановленного компонента показывает свой фолбэк. Когда промис разрешится, React прочитает значение через use и заменит фолбэк отрендеренным компонентом.

Примеры

Загрузка данных с помощью use

В этом примере Albums вызывает use с закэшированным промисом. Пока промис ожидает, компонент приостанавливается, и React показывает ближайший фолбэк Suspense. Отклонённые промисы доходят до ближайшей границы ошибки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { use, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { fetchData } from './data.js';

export default function App() {
    return (
        <ErrorBoundary fallback={<p>Could not fetch albums.</p>}>
            <Suspense fallback={<Loading />}>
                <Albums />
            </Suspense>
        </ErrorBoundary>
    );
}

function Albums() {
    const albums = use(fetchData('/albums'));
    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}

function Loading() {
    return <h2>Loading...</h2>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();

export function fetchData(url) {
    if (!cache.has(url)) {
        cache.set(url, getData(url));
    }
    return cache.get(url);
}

async function getData(url) {
    if (url === '/albums') {
        return await getAlbums();
    } else {
        throw Error('Not implemented');
    }
}

async function getAlbums() {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    return [{
        id: 13,
        title: 'Let It Be',
        year: 1970
    }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
    }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
    }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
    }];
}
1
2
3
4
5
6
7
8
9
{
  "dependencies": {
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-scripts": "^5.0.0",
    "react-error-boundary": "4.0.3"
  },
  "main": "/index.js"
}
Решение

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState, useEffect } from 'react';
import { fetchAlbums } from './data.js';

export default function App() {
    const [albums, setAlbums] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetchAlbums()
            .then(data => {
                setAlbums(data);
                setIsLoading(false);
            })
            .catch(err => {
                setError(err);
                setIsLoading(false);
            });
    }, []);

    if (isLoading) {
        return <h2>Loading...</h2>;
    }

    if (error) {
        return <p>Error: {error.message}</p>;
    }

    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export async function fetchAlbums() {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    return [{
        id: 13,
        title: 'Let It Be',
        year: 1970
    }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
    }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
    }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
    }];
}
Решение

Промисы, переданные в use, нужно кэшировать

Промисы, созданные во время рендера, создаются заново при каждом рендере. Из-за этого React снова и снова показывает фолбэк Suspense, и содержимое не появляется.

1
2
3
4
5
function Albums() {
  // 🔴 `fetch` creates a new Promise on every render.
  const albums = use(fetch('/albums'));
  // ...
}

Вместо этого передайте промис из кэша, фреймворка с поддержкой Suspense или серверного компонента:

1
2
// ✅ fetchData reads the Promise from a cache.
const albums = use(fetchData('/albums'));
Почему промисы создаются заново при каждом рендере?

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Albums() {
  // 🔴 `fetch` creates a new Promise on every render.
  const albums = use(fetch('/albums'));

  // 🔴 Uncached `async` function calls create a new Promise on every render.
  const albums = use((async () => {
    const res = await fetch('/albums');
    return res.json();
  })());

  // 🔴 Adding `.then` returns a new Promise on every render,
  // even if `fetchData` is cached.
  const albums = use(fetchData('/albums').then(res => res.json()));
  // ...
}

В идеале промисы создают до рендера: в обработчике события, загрузчике маршрута или серверном компоненте — и передают в компонент, который вызывает use. Ленивая загрузка во время рендера откладывает сетевые запросы и может создать водопады.

1
2
// ✅ fetchData reads the Promise from a cache.
const albums = use(fetchData('/albums'));

Кэширование промисов для клиентских компонентов

Промисы, переданные в use в клиентских компонентах, нужно кэшировать, чтобы между повторными рендерами переиспользовался один и тот же экземпляр. Если новый промис создаётся прямо в рендере, React будет показывать фолбэк Suspense при каждом повторном рендере.

1
2
3
4
5
6
7
8
9
// ✅ Cache the Promise so the same one is reused across renders
let cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, getData(url));
  }
  return cache.get(url);
}

Функция fetchData возвращает один и тот же промис при каждом вызове с тем же URL. Когда use при повторном рендере получает тот же промис, он синхронно читает уже разрешённое значение и не приостанавливается.

Примечание

Как кэшировать промисы, зависит от фреймворка, с которым вы используете Suspense. Обычно у фреймворков есть встроенное кэширование. Если фреймворка нет, подойдёт простой кэш на уровне модуля, как выше, или источник данных с поддержкой Suspense.

В примере ниже клик по «Re-render» обновляет состояние в App и вызывает повторный рендер. Поскольку fetchData возвращает тот же закэшированный промис, Albums читает значение синхронно и не показывает фолбэк Suspense снова.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { use, Suspense, useState } from 'react';
import { fetchData } from './data.js';

export default function App() {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={() => setCount(count + 1)}>
                Re-render
            </button>
            <p>Render count: {count}</p>
            <Suspense fallback={<p>Loading...</p>}>
                <Albums />
            </Suspense>
        </>
    );
}

function Albums() {
    const albums = use(fetchData('/albums'));
    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();

export function fetchData(url) {
    if (!cache.has(url)) {
        cache.set(url, getData(url));
    }
    return cache.get(url);
}

async function getData(url) {
    if (url === '/albums') {
        return await getAlbums();
    } else {
        throw Error('Not implemented');
    }
}

async function getAlbums() {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    return [{
        id: 13,
        title: 'Let It Be',
        year: 1970
    }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
    }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
    }];
}

Как реализовать кэш промисов

Простой кэш хранит промис по ключу URL, чтобы между рендерами переиспользовался один экземпляр. Чтобы не показывать лишний фолбэк Suspense, когда данные уже есть, на промисе можно выставить поля status и value (или reason). React смотрит эти поля, когда вызывается use: если status равен 'fulfilled', он синхронно читает value и не приостанавливается. Если status равен 'rejected', он бросает reason. Если поля нет или оно равно 'pending', он приостанавливается.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let cache = new Map();

function fetchData(url) {
  if (!cache.has(url)) {
    const promise = getData(url);
    promise.status = 'pending';
    promise.then(
      value => {
        promise.status = 'fulfilled';
        promise.value = value;
      },
      reason => {
        promise.status = 'rejected';
        promise.reason = reason;
      },
    );
    cache.set(url, promise);
  }
  return cache.get(url);
}

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

Этот кэш — основа для повторной загрузки данных (смена ключа кэша запускает новую загрузку) и предзагрузки при наведении (ранний вызов fetchData значит, что к моменту чтения через use промис уже может быть разрешён).

Не пропускайте вызов use из-за того, что промис уже завершился

В отличие от других хуков, use можно вызывать в условиях и циклах, но для самого промиса его нужно вызывать всегда. Никогда не читайте promise.status или promise.value напрямую, чтобы обойти use: всегда передавайте промис в use и дайте React разобраться.

1
2
3
4
5
// 🔴 Don't bypass `use` by reading promise status directly
if (promise.status === 'fulfilled') {
  return promise.value;
}
const value = use(promise);
1
2
// ✅ Pass the promise to `use` and let React track the promise
const value = use(promise);

Такой обход может сломать оптимизации Suspense и возможности Suspense в React DevTools. use(promise) можно вызывать условно, но нельзя решать, вызывать ли use(promise), по самому промису.

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

Чтобы обновить данные по тому же URL (например, кнопкой «Refresh»), сбросьте запись кэша и начните новую загрузку внутри startTransition. Сохраните полученный промис в состоянии, чтобы вызвать повторный рендер. Пока новый промис ожидает, React продолжает показывать текущее содержимое, потому что обновление внутри перехода.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function App() {
  const [albumsPromise, setAlbumsPromise] = useState(fetchData('/albums'));
  const [isPending, startTransition] = useTransition();

  function handleRefresh() {
    startTransition(() => {
      setAlbumsPromise(refetchData('/albums'));
    });
  }
  // ...
}

refetchData очищает старую запись кэша и начинает новую загрузку по тому же URL. Сохранение полученного промиса в состоянии вызывает повторный рендер внутри перехода. При повторном рендере Albums получает новый промис, и use приостанавливается на нём, а React продолжает показывать старое содержимое.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Suspense, useState, useTransition } from 'react';
import { use } from 'react';
import { fetchData, refetchData } from './data.js';

export default function App() {
    const [albumsPromise, setAlbumsPromise] = useState(
        () => fetchData('/the-beatles/albums')
    );
    const [isPending, startTransition] = useTransition();

    function handleRefresh() {
        startTransition(() => {
            setAlbumsPromise(refetchData('/the-beatles/albums'));
        });
    }

    return (
        <>
            <button
                onClick={handleRefresh}
                disabled={isPending}
            >
                {isPending ? 'Refreshing...' : 'Refresh'}
            </button>
            <div style={{ opacity: isPending ? 0.6 : 1 }}>
                <Suspense fallback={<Loading />}>
                    <Albums albumsPromise={albumsPromise} />
                </Suspense>
            </div>
        </>
    );
}

function Albums({ albumsPromise }) {
    const albums = use(albumsPromise);
    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}

function Loading() {
    return <h2>Loading...</h2>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();

export function fetchData(url) {
    if (!cache.has(url)) {
        cache.set(url, getData(url));
    }
    return cache.get(url);
}

export function refetchData(url) {
    cache.delete(url);
    return fetchData(url);
}

async function getData(url) {
    if (url.startsWith('/the-beatles/albums')) {
        return await getAlbums();
    } else {
        throw Error('Not implemented');
    }
}

async function getAlbums() {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
        setTimeout(resolve, 1000);
    });

    return [{
        id: 13,
        title: 'Let It Be',
        year: 1970
    }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
    }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
    }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
    }, {
        id: 9,
        title: 'Magical Mystery Tour',
        year: 1967
    }];
}
1
button { margin-bottom: 10px; }

Примечание

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

Предзагрузка данных при наведении

Данные можно начать грузить до того, как они понадобятся, вызвав fetchData при наведении. Поскольку fetchData кэширует промис, к моменту клика данные уже могут быть на месте. Если к моменту чтения через use промис разрешился, React сразу рендерит компонент и не показывает фолбэк Suspense.

1
2
3
4
5
6
7
8
<button
  onMouseEnter={() => fetchData(`/${id}/albums`)}
  onClick={() => {
    startTransition(() => {
      setArtistId(id);
    });
  }}
>

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Suspense, useState, useTransition } from 'react';
import Albums from './Albums.js';
import { fetchData } from './data.js';

export default function App() {
    const [artistId, setArtistId] = useState('the-beatles');
    const [isPending, startTransition] = useTransition();

    return (
        <>
            <div>
                {['the-beatles', 'led-zeppelin', 'pink-floyd'].map(id => (
                    <button
                        key={id}
                        onMouseEnter={() => {
                            fetchData(`/${id}/albums`);
                        }}
                        onClick={() => {
                            startTransition(() => {
                                setArtistId(id);
                            });
                        }}
                    >
                        {id === 'the-beatles' ? 'The Beatles' :
                          id === 'led-zeppelin' ? 'Led Zeppelin' :
                          'Pink Floyd'}
                    </button>
                ))}
            </div>
            <Suspense key={artistId} fallback={<Loading />}>
                <Albums artistId={artistId} />
            </Suspense>
        </>
    );
}

function Loading() {
    return <h2>Loading...</h2>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { use } from 'react';
import { fetchData } from './data.js';

export default function Albums({ artistId }) {
    const albums = use(fetchData(`/${artistId}/albums`));
    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();

export function fetchData(url) {
    if (!cache.has(url)) {
        const promise = getData(url);
        // Set status fields so React can read the value
        // synchronously if the Promise resolves before
        // `use` is called (e.g. when preloading on hover).
        promise.status = 'pending';
        promise.then(
            value => {
                promise.status = 'fulfilled';
                promise.value = value;
            },
            reason => {
                promise.status = 'rejected';
                promise.reason = reason;
            },
        );
        cache.set(url, promise);
    }
    return cache.get(url);
}

async function getData(url) {
    if (url.startsWith('/the-beatles/albums')) {
        return await getAlbums('the-beatles');
    } else if (url.startsWith('/led-zeppelin/albums')) {
        return await getAlbums('led-zeppelin');
    } else if (url.startsWith('/pink-floyd/albums')) {
        return await getAlbums('pink-floyd');
    } else {
        throw Error('Not implemented');
    }
}

async function getAlbums(artistId) {
    // Add a fake delay to make waiting noticeable.
    await new Promise(resolve => {
        setTimeout(resolve, 800);
    });

    if (artistId === 'the-beatles') {
        return [{
            id: 13,
            title: 'Let It Be',
            year: 1970
        }, {
            id: 12,
            title: 'Abbey Road',
            year: 1969
        }, {
            id: 11,
            title: 'Yellow Submarine',
            year: 1969
        }];
    } else if (artistId === 'led-zeppelin') {
        return [{
            id: 10,
            title: 'Coda',
            year: 1982
        }, {
            id: 9,
            title: 'In Through the Out Door',
            year: 1979
        }, {
            id: 8,
            title: 'Presence',
            year: 1976
        }];
    } else {
        return [{
            id: 7,
            title: 'The Wall',
            year: 1979
        }, {
            id: 6,
            title: 'Animals',
            year: 1977
        }, {
            id: 5,
            title: 'Wish You Were Here',
            year: 1975
        }];
    }
}
1
button { margin-right: 10px; }

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { fetchMessage } from './lib.js';
import { Message } from './message.js';

export default function App() {
    const messagePromise = fetchMessage();
    return (
        <Suspense fallback={<p>waiting for message...</p>}>
            <Message messagePromise={messagePromise} />
        </Suspense>
    );
}

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

1
2
3
4
5
6
7
8
9
// message.js
'use client';

import { use } from 'react';

export function Message({ messagePromise }) {
    const messageContent = use(messagePromise);
    return <p>Here is the message: {messageContent}</p>;
}

Поскольку Message обернут в Suspense, fallback будет отображаться до тех пор, пока Promise не будет разрешен. Когда обещание будет разрешено, значение будет считано хуком use и компонент Message заменит фаллбэк Suspense.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
'use client';

import { use, Suspense } from 'react';

function Message({ messagePromise }) {
    const messageContent = use(messagePromise);
    return <p>Here is the message: {messageContent}</p>;
}

export function MessageContainer({ messagePromise }) {
    return (
        <Suspense
            fallback={<p>⌛Downloading message...</p>}
        >
            <Message messagePromise={messagePromise} />
        </Suspense>
    );
}

Сериализуемость значений

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

Как разрешить промис в серверном или клиентском компоненте?

Промис можно передать из серверного компонента в клиентский компонент и разрешить его в клиентском компоненте с помощью хука use. Вы также можете разрешить промис в серверном компоненте с помощью await и передать необходимые данные клиентскому компоненту в качестве свойства.

1
2
3
4
export default function App() {
    const messageContent = await fetchMessage();
    return <Message messageContent={messageContent} />
}

Но использование await в компоненте Server Component заблокирует его рендеринг до завершения оператора await. Передача промиса от серверного компонента клиентскому компоненту не позволяет промису блокировать отрисовку серверного компонента.

Отображение ошибки с помощью границы ошибки

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

В примере ниже fetchData отклоняется при первой попытке и успешно завершается при повторе. Граница ошибки перехватывает отказ и показывает фолбэк с кнопкой «Try again».

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { use, Suspense, useState, startTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { fetchData, refetchData } from "./data.js";

export default function App() {
    const [albumsPromise, setAlbumsPromise] = useState(
        () => fetchData('/the-beatles/albums')
    );

    function handleRetry() {
        startTransition(() => {
            setAlbumsPromise(refetchData('/the-beatles/albums'));
        });
    }

    return (
        <ErrorBoundary
            resetKeys={[albumsPromise]}
            fallbackRender={() => (
                <>
                    <p>⚠️ Something went wrong loading the albums.</p>
                    <button onClick={handleRetry}>Try again</button>
                </>
            )}
        >
            <Suspense fallback={<p>Loading...</p>}>
                <Albums albumsPromise={albumsPromise} />
            </Suspense>
        </ErrorBoundary>
    );
}

function Albums({ albumsPromise }) {
    const albums = use(albumsPromise);
    return (
        <ul>
            {albums.map(album => (
                <li key={album.id}>
                    {album.title} ({album.year})
                </li>
            ))}
        </ul>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();
let retried = false;

export function fetchData(url) {
    if (!cache.has(url)) {
        cache.set(url, getData(url));
    }
    return cache.get(url);
}

export function refetchData(url) {
    cache.delete(url);
    retried = true;
    return fetchData(url);
}

async function getData(url) {
    // Add a fake delay to make the loading state visible.
    await new Promise(resolve => setTimeout(resolve, 1000));
    if (url === '/the-beatles/albums') {
        // Fail the first attempt to demonstrate the Error Boundary,
        // then succeed on retry.
        if (!retried) {
            throw new Error('Example Error: Failed to fetch albums');
        }
        return [{
            id: 13,
            title: 'Let It Be',
            year: 1970
        }, {
            id: 12,
            title: 'Abbey Road',
            year: 1969
        }, {
            id: 11,
            title: 'Yellow Submarine',
            year: 1969
        }, {
            id: 10,
            title: 'The Beatles',
            year: 1968
        }];
    }
    throw new Error('Not implemented');
}
1
2
3
4
5
6
7
8
9
{
  "dependencies": {
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-scripts": "^5.0.0",
    "react-error-boundary": "4.0.3"
  },
  "main": "/index.js"
}

Использование (браузер)

Рендер компонента только в браузере

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

Нажмите Reload, чтобы увидеть фолбэк загрузки в исходном HTML. После гидратации React показывает черновик, загруженный из localStorage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { Suspense, use, useState } from 'react';
import { browser } from 'react-dom';

function SavedDraft() {
    use(browser('The draft is stored in localStorage.'));
    const [draft, setDraft] = useState(
        () => localStorage.getItem('draft') ?? ''
    );

    function handleChange(event) {
        const nextDraft = event.target.value;
        setDraft(nextDraft);
        localStorage.setItem('draft', nextDraft);
    }

    return (
        <label>
            Draft:
            <textarea
                value={draft}
                onChange={handleChange}
                rows={4}
                cols={30}
            />
        </label>
    );
}

export default function App() {
    return (
        <>
            <h1>Saved draft</h1>
            <Suspense fallback={<p>Loading draft...</p>}>
                <SavedDraft />
            </Suspense>
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import App from './App.js';

export default function Document() {
    return (
        <html lang="en">
            <head>
                <title>Saved draft</title>
                <style>{`
                    h1 { font-size: 24px; margin-top: 0; }
                    label, textarea { display: block; }
                    textarea { margin-top: 5px; }
                `}</style>
            </head>
            <body>
                <App />
            </body>
        </html>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export async function flushReadableStreamToFrame(readable, frame) {
    const doc = frame.contentWindow.document;
    const decoder = new TextDecoder();
    const reader = readable.getReader();

    while (true) {
        const {done, value} = await reader.read();
        if (done) {
            break;
        }
        doc.write(decoder.decode(value, {stream: true}));
    }

    doc.write(decoder.decode());
    doc.close();
}
1
2
3
4
5
iframe {
  width: 100%;
  height: 160px;
  border: 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

Во время серверного рендеринга use(browser()) приостанавливает компонент, и React включает в HTML фолбэк ближайшей границы приостановки. В браузере use(browser()) возвращает undefined, и сохранённый черновик рендерится как обычно.

Работа с отклоненными промисами

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

  1. Отображение ошибки для пользователей с границей ошибки.
  2. Предоставить альтернативное значение с помощью Promise.catch

Отображение ошибки для пользователей с границей ошибки

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use client';

import { use, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

export function MessageContainer({ messagePromise }) {
    return (
        <ErrorBoundary
            fallback={<p>⚠️Something went wrong</p>}
        >
            <Suspense
                fallback={<p>⌛Downloading message...</p>}
            >
                <Message messagePromise={messagePromise} />
            </Suspense>
        </ErrorBoundary>
    );
}

function Message({ messagePromise }) {
    const content = use(messagePromise);
    return <p>Here is the message: {content}</p>;
}

Предоставление альтернативного значения с помощью Promise.catch

Если вы хотите предоставить альтернативное значение, когда промис, переданный в use, будет отклонен, вы можете использовать метод catch промиса.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Message } from './message.js';

export default function App() {
    const messagePromise = new Promise(
        (resolve, reject) => {
            reject();
        }
    ).catch(() => {
        return 'no new message found.';
    });

    return (
        <Suspense fallback={<p>waiting for message...</p>}>
            <Message messagePromise={messagePromise} />
        </Suspense>
    );
}

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

Устранение неполадок

"Suspense Exception: This is not a real error!"

Вы либо вызываете use вне компонента React или функции Hook, либо вызываете use в блоке try-catch. Если вы вызываете use внутри блока try-catch, оберните ваш компонент в границу ошибки или вызовите catch промиса, чтобы поймать ошибку и разрешить промис другим значением. См. эти примеры.

Если вы вызываете use вне компонента React или функции Hook, перенесите вызов use в компонент React или функцию Hook.

1
2
3
4
5
function MessageComponent({messagePromise}) {
  function download() {
    // ❌ the function calling `use` is not a Component or Hook
    const message = use(messagePromise);
    // ...

Вместо этого вызывайте use вне закрытий компонентов, если функция, вызывающая use, является компонентом или хуком.

1
2
3
4
function MessageComponent({messagePromise}) {
  // ✅ `use` is being called from a component.
  const message = use(messagePromise);
  // ...

Предупреждение: «A component was suspended by an uncached promise»

Промис, переданный в use, не закэширован, поэтому React не может переиспользовать его между повторными рендерами.

Так часто бывает, если fetch или async-функцию вызывают прямо в рендере:

1
2
3
4
5
function Albums() {
  // 🔴 This creates a new Promise on every render
  const albums = use(fetch('/albums'));
  // ...
}

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

1
2
// ✅ fetchData returns the same Promise for the same URL
const albums = use(fetchData('/albums'));

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

Источник — https://react.dev/reference/react/use

Комментарии