<Suspense> дає вам змогу відображати запасний варіант (fallback), доки його дочірні компоненти не завершать завантаження.

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

Опис

<Suspense>

Пропси

  • children: Очікуваний UI, який ви хочете відрендерити. Якщо children затримується (suspends) під час рендерингу, межа (boundary) Suspense перемкнеться на рендер fallback.
  • fallback: Альтернативний UI, який рендериться замість очікуваного UI, якщо той ще не завершив завантаження. Проп приймає будь-який валідний React-вузол, хоча на практиці запасний варіант є невеличким елементом для заповнення області перегляду, як-от спінер чи скелетон. Suspense автоматично перемкнеться на fallback, коли children затримується, і назад на children, коли дані будуть готові. Якщо fallback затримується під час рендеру, найближча батьківська межа Suspense буде активована.

Застереження

  • React не зберігає жодного стану для рендерів, затриманих до першого монтування (mount). Коли компонент завантажиться, React ще раз спробує відрендерити затримане дерево компонентів із нуля.
  • Якщо Suspense відображав вміст, але затримався повторно, fallback буде відображено знову, за винятком випадків, коли оновлення, яке це спричинило, зумовлене функціями startTransition або useDeferredValue.
  • Якщо React потрібно сховати вже видимий вміст через повторну затримку, він скине ефекти макета в дереві компонентів. Коли вміст буде знову готовий до показу, React викличе ефекти макета знову. Це запевняє, що ефекти, які проводять виміри DOM-макета, не намагатимуться робити цього, доки вміст прихований.
  • React має вбудовані оптимізації інтегровані в Suspense, як-от Потоковий рендеринг на стороні сервера і Вибіркову гідрацію. Прочитайте архітектурний огляд і подивіться технічну доповідь, щоб дізнатися більше.

Використання

Відображення запасного варіанту під час завантаження вмісту

Ви можете загорнути будь-яку частину вашого застосунку в межу Suspense:

<Suspense fallback={<Loading />}>
<Albums />
</Suspense>

React відображатиме ваш запасний варіант завантаження, доки всесь код та дані, які потребує дочірній компонент, не будуть завантажені.

У прикладі вище, компонент Albums затримується під час отримання списку альбомів. Доки він не буде готовим до рендеру, React переключиться на найближчу межу Suspense вверху дерева, щоб показати запасний варіант - ваш компонент Loading. Коли дані завантажаться, React сховає запасний варіант Loading і відрендерить компонент Albums з даними.

import { Suspense } from 'react';
import Albums from './Albums.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Завантаження...</h2>;
}

Note

Тільки джерела даних із підтримкою Suspense активують компонент Suspense. Вони включають:

  • Отримання даних із фреймворками, що підтримують Suspense, наприклад, Relay та Next.js
  • Компоненти з відкладеним завантаженням, які використовують lazy
  • Зчитування значення Promise з use

Suspense не реагує, коли дані отримуються всередині ефекта чи обробника подій.

Спосіб, у який ви будете отримувати дані в компоненті Albums, наведеному вище, залежить від вашого фреймоворку. Якщо ви використовуєте фреймворк із підтримкою Suspense, ви знайдете деталі в його документації щодо отримання даних.

Отримання даних із Suspense, але без використання фреймворку, наразі не підтримується. Вимоги до реалізації джерела даних із підтримкою Suspense нестабільні й незадокументовані. Офіційний API для інтеграції джерел даних та Suspense буде випущено в майбутній версії React.


Одночасне відображення всього вмісту

Початково, усе дерево всередині Suspense сприймається як один компонент. Для прикладу, навіть якщо тільки один із цих компонентів затримується, очікуючи на дані, всі з них буде замінено на індикатор завантаження:

<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>

Коли всі з них будуть готові до відображення, вони зв’являться всі разом в один момент.

У прикладі нижче, обидва компоненти Biography і Albums отримують якісь дані. Проте, через те що вони згруповані всередині одної межі Suspense, ці компоненти завжди “вискакуватимуть” одночасно.

import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Завантаження...</h2>;
}

Компоненти, що завантажують дані, можуть не бути прямими дочірніми компонентами межі Suspense. Наприклад, ви можете перенести Biography і Albums у новий компонент Details. Це не вплине на поведінку. Biography і Albums поділяють одну найближчу батьківську межу Suspense, тому їхнє відображення координується разом.

<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>

function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}

Відображення вкладеного вмісту поступово, відповідно до його завантаження

Коли компонент затримується, найближчий батьківський компонент Suspense відображає запасний варіант. Це дає вам змогу вкладувати кілька компонентів Suspense, щоб сворити послідовність завантаження. Кожен запасний варіант Suspense буде замінено, коли наступний рівень вмісту буде доступним. Наприклад, ви можете дати списку альбомів власний запасний варіант:

<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>

Із цією зміною, для відображення Biography не потрібно “чекати” завантаження Albums.

Послідовність буде такою:

  1. Якщо Biography ще не завантажився, BigSpinner буде показано замість усього вмісту.
  2. Як тільки Biography закінчить завантаження, BigSpinner буде замінено бажаним вмістом.
  3. Якщо Albums ще не завантажився, AlbumsGlimmer буде показано замість Albums і його батьківського компонента Panel.
  4. Нарешті, як тільки Albums закінчить завантаження, він замінить AlbumsGlimmer.
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<BigSpinner />}>
        <Biography artistId={artist.id} />
        <Suspense fallback={<AlbumsGlimmer />}>
          <Panel>
            <Albums artistId={artist.id} />
          </Panel>
        </Suspense>
      </Suspense>
    </>
  );
}

function BigSpinner() {
  return <h2>🌀 Завантаження...</h2>;
}

function AlbumsGlimmer() {
  return (
    <div className="glimmer-panel">
      <div className="glimmer-line" />
      <div className="glimmer-line" />
      <div className="glimmer-line" />
    </div>
  );
}

Межі Suspense дають вам змогу контролювати, які частини UI повинні завжди з’являтися одночасно, і які частини повинні поступово показувати більше вмісту відповідно до послідовності завантаження. Ви можете додавати, переставляти або видаляти межі Suspense в будь-якому місці дерева компонетів, без впливу на поведінку решти застосунку.

Не ставте межу Suspense навколо кожного компонента. Межі Suspense не повинні бути більш частими, ніж послідовність завантаження, яку ви хочете, щоб користувач побачив. Якщо ви працюєте з дизайнером, запитайте його, де повинні відображатися індикатори завантаження — висока вірогідність, що вони вже включили їх у макети дизайну.


Відображення застарілого вмісту під час завантаження нового

У цьому прикладі, компонент SearchResults затримується, доки завантажує результати пошуку. Введіть "a", зачекайте на результат, а тоді змініть на "ab". Результати для "a" будуть замінені запасним варінтом завантаження.

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Пошук альбомів:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Завантаження...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

Поширеним альтернативним UI паттерном є відкладене оновлення списку з показом попередніх результатів, доки нові результати не будуть готові. Хук useDeferredValue дає змогу передавати відкладений варіант запиту вниз по дереву:

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Пошук альбомів:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Завантаження...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

query оновиться одразу, тому пошуковий рядок відображатиме нове значення. Проте, deferredQuery збереже попереднє значення, доки дані не будуть завантажені, тож SearchResults на деякий час відобразить застарілі результати.

Щоб зробити це більш очевидним для користувача, ви можете додати візуальний індикатор під час відображення застарілого вмісту:

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>

Введіть "a" у прикладі нижче, зачекайте на результат, тоді змініть значення на "ab". Зверніть увагу, як замість запасного варіанту, ви бачите затемнений список попередніх результатів, доки нові результати не завантажилися:

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Пошук альбомів:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Завантаження...</h2>}>
        <div style={{ opacity: isStale ? 0.5 : 1 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

Note

Як затримані значення так і переходи дають вам змогу уникнути відображення запасного варіанту Suspense, натомість відображаючи індикатор безпосередньо у вмісті. Переходи відмічають оновлення як нетермінові, тож вони часто використовуються фреймворками та бібліотеками-маршрутизаторами для навігації. З іншого боку, відкладені значення, переважно використовуються в коді застосунку там, де ви хочете відмітити частину UI як нетермінову й дозволити їй “відставати” від решти UI.


Запобігання заміни запасним варіантом уже відображеного вмісту

Коли компонент затримується, найближча батьківська межа Suspense перемикається на показ запасного варіанту. Це може призвести до неприємного користувацького досвіду у випадку, якщо якийсь вміст уже відображався. Спробуйте настинути цю кнопку:

import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    setPage(url);
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Завантаження...</h2>;
}

Коли ви натиснули кнопку, компонент Router відрендерив ArtistPage замість IndexPage. Компонент всередині ArtistPage затриманий, тож найближча межа Suspense почала відображати запасний варіант. Найближча межа Suspense була біля корневого компонента, тому весь макет сайту було замінено на BigSpinner.

Щоб запобігти цьому, ви можете відмітити оновлення стану навігації як перехід, використовуючи startTransition:

function Router() {
const [page, setPage] = useState('/');

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

Це говорить React що перехід стану не є терміновим і краще продовжити показувати попередню сторінку, замість того, щоб ховати вже відображений вміст. Тепер натискання на кнопку “очікує”, доки Biography завантажиться:

import { Suspense, startTransition, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Завантаження...</h2>;
}

Перехід не чекає на завантаження всього вмісту. Він лише чекає достатньо довго, щоб уникнути приховання вже відображеного вмісту. Для прикладу, Layout вебсайту вже було відображено, тому було би погано ховати його за спіннером завантаження. Проте, вкладена межа Suspense навколо Albums нова, тому перехід не чекає на неї.

Note

Передбачається, що маршрутизатори з інтегрованим Suspense заздалегідь огортатимуть оновлення навігації в перехід.


Індикація переходу

У прикладі зверху, як тільки ви натискаєте на кнопку, відсутній візуальний сигнал того, що відбувається навігація. Щоб додати індикатор, ви можете замінити startTransition на useTransition, який дає вам булеве значення isPending. У прикладі нище, воно використовується, щоб змінити стилі хедеру вебсайту, доки відбувається перехід:

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}


Скидання меж Suspense під час навігації

Під час переходу, React уникне приховання вже відображеного вмісту. Проте, якщо ви перейдете на маршрут з іншими параметрами, ви захочете сказати React, що це інший вміст. Ви можете досягнути цього з key:

<ProfilePage key={queryParams.id} />

Уявіть, що ви переходите всередині сторінки профілю користувача, і щось затримується. Якщо те оновлення використовує перехід, воно не буде викликати запасний варіант для вже відображеного вмісту. Така поведінка є очікуваною.

А тепер уявіть, що ви переходите між профілями двох різних користувачів. У такому випадку доцільно відображати запасний варіант. Наприклад, вміст стрічки одного користувача відрізняється, від стрічки іншого користувача. Вказуючи key, ви запевняєтеся, що React розглядає профілі різних користувачів як різні компоненти і скидає межу Suspense під час навігації. Маршрутизатори з інтегрованим Suspense повинні робити це автоматично.


Застосування запасного варіанту для серверних помилок та вмісту, що опрацьовується тільки на стороні клієнта

Якщо ви використовуєте якийсь з API для потокового рендеру на стороні сервера (або фреймворк, що покладається на них), React також використовуватими вашу межу <Suspense>, щоб обробляти помилки на стороні сервера. Якщо компонент видає помилку на стороні сервера, React не відмінить серверний рендеринг. Натомість, він знайде найближчий компонент <Suspense> вище нього й додасть його запасний варіант (наприклад спіннер) у згенерований сервером HTML. Спочатку користувач побачить спіннер.

На стороні клієнта, React спробує відрендерити той же компонент знову. Якщо в ньому виникає помилка й на стороні клієнта, React видасть помилку і відобразить найближчу границю помилки. Проте, якщо він не видає помилки на стороні клієнта, React не буде відображати користувачу помилку, тому що вміст усе ж був відображений коректно.

Ви можете використати це, щоб виключити деякі компоненти з рендерингу на стороні сервера. Щоб зробити це, видайте помилку в серверному оточенні й обгорніть ці компоненти в межу <Suspense>, щоб замінити їхній HTML запасним варіантом:

<Suspense fallback={<Loading />}>
<Chat />
</Suspense>

function Chat() {
if (typeof window === 'undefined') {
throw Error('Чат повинен рендеритися тільки на стороні клієнта.');
}
// ...
}

Відрендерений на стороні сервера HTML включатиме лише індикатор завантаження. Його буде замінено компонентом Chat на стороні клієнта.


Усунення неполадок

Як я можу запобігти заміні UI запасним варіантом під час оновлення?

Заміна видимого UI запасним варіантом спричиняє неприємний користувацький досвід. Це стається, коли оновлення спричиняє затримку компонента, а найближчий Suspense вже показує вміст користувачу.

Щоб запобігти цьому, відмітьте оновлення нетерміновим, використовуючи startTransition. Під час переходу, React зачекає на завантаження даних, щоб запобігти відображенню небажаного запасного варіанту:

function handleNextPageClick() {
// Якщо це оновлення затримається, уже відображений вміст не буде сховано
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}

Це допоможе уникнути приховання вже існуючого вмісту. Однак, будь-яка наново відрендерена межа Suspense, усе ще відображатиме запасний варіант, щоб уникнути блокування UI і дасть змогу користувачу бачити вміст як тільки він стане доступним.

React запобігатиме небажаним запасним варіантам лише під час нетермінових оновлень. Він не затримуватиме рендеринг, якщо це результат термінового оновлення. Ви повинні увімкнути це з API, наприклад, startTransition або useDeferredValue.

Якщо у ваш маршрутизатор інтегровано Suspense, він повинен огортати оновлення у startTransition автоматично.