Spec-Zone.ru › React 18

<Suspense>

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

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>
  • Ссылка
    • <Suspense>
  • Использование
    • Отображение резервного варианта во время загрузки контента
    • Одновременное раскрытие контента
    • Раскрытие вложенного контента по мере загрузки
    • Отображение устаревшего контента во время загрузки нового
    • Предотвращение скрытия уже отображенного контента
    • Указание на то, что происходит переход
    • Сброс границ Suspense при навигации
    • Предоставление резервного варианта для ошибок сервера и контента, доступного только на стороне клиента
  • Отладка
    • Как предотвратить замену пользовательского интерфейса резервным вариантом во время обновления?

Ссылка

<Suspense>

Свойства

  • children: фактический пользовательский интерфейс, который вы хотите отобразить. Если children приостанавливает рендеринг, граница Suspense переключится на отображение fallback.
  • fallback: альтернативный пользовательский интерфейс для отображения вместо фактического пользовательского интерфейса, если он не завершил загрузку. Принимается любой допустимый узел React, хотя на практике резервный вариант — это легкая заглушка, например, индикатор загрузки или скелет. Suspense автоматически переключится на fallback при приостановке children, и обратно на children при готовности данных. Если fallback приостановит рендеринг, это активирует ближайшую родительскую границу Suspense.

Ограничения

  • React не сохраняет состояние для рендерингов, которые были приостановлены до момента первой отрисовки. После загрузки компонента 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>🌀 Loading...</h2>;
}

Примечание

Только источники данных с поддержкой 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>🌀 Loading...</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>🌀 Loading...</h2>;
}

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

Границы Suspense позволяют координировать, какие части вашего пользовательского интерфейса всегда «появляются» одновременно, а какие части постепенно раскрывают больше контента в последовательности состояний загрузки. Вы можете добавлять, перемещать или удалять границы 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>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

Распространенный альтернативный шаблон пользовательского интерфейса — это отложенное обновление списка и отображение предыдущих результатов до тех пор, пока новые результаты не будут готовы. Хук useDeferredValue позволяет передавать отложенную версию запроса вниз:

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

query будет обновляться немедленно, поэтому вход будет отображать новое значение. Однако deferredQuery сохранит свое предыдущее значение до загрузки данных, поэтому SearchResults отобразит устаревшие результаты в течение некоторого времени.

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

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

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

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>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{ opacity: isStale ? 0.5 : 1 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

Примечание

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


Предотвращение скрытия уже отображенного контента

При приостановке компонента ближайшая родительская граница 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>🌀 Loading...</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>🌀 Loading...</h2>;
}

Переход не ждет загрузки всего содержимого. Он ждет достаточно долго, чтобы избежать скрытия уже отображенного содержимого. Например, веб-сайт Layout уже был отображен, поэтому было бы неправильно скрывать его за индикатором загрузки. Однако вложенная Suspense граница вокруг Albums является новой, поэтому Переход не ждет ее загрузки.

Примечание

Маршрутизаторы, поддерживающие 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('Chat should only render on the client.');
  }
  // ...
}

Серверный HTML будет включать индикатор загрузки. Он будет заменен на компонент Chat на клиенте.


Отладка

Как предотвратить замену интерфейса резервным вариантом при обновлении?

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

Чтобы предотвратить это, отметьте обновление как некритическое, используя startTransition. Во время перехода React будет ждать, пока загрузится достаточно данных, чтобы предотвратить появление нежелательного резервного варианта:

function handleNextPageClick() {
  // If this update suspends, don't hide the already displayed content
  startTransition(() => {
    setCurrentPage(currentPage + 1);
  });
}

Это позволит избежать скрытия существующего содержимого. Однако любые недавно отображенные Suspense границы все равно немедленно будут отображать резервные варианты, чтобы не блокировать интерфейс и позволить пользователю увидеть содержимое по мере его доступности.

React будет предотвращать только нежелательные резервные варианты при некритических обновлениях. Он не будет задерживать рендеринг, если это результат срочного обновления. Вы должны использовать API, такой как startTransition или useDeferredValue.

Если ваш маршрутизатор интегрирован с Suspense, он должен автоматически заключать свои обновления в startTransition.

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/Suspense

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API