<Suspense>
<Suspense>
дозволяє вам відображати запасний варіант, доки його дочірні компоненти не завершать завантаження.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Опис
- Використання
- Відображення запасного варіанту, доки вміст завантажується
- Одночасне відображення всього вмісту
- Відображення вкладеного вмісту по мірі його завантаження
- Відображення існуючого вмісту, доки новий вміст завантажується
- Запобігання заміни запасним варіантом уже відображеного вмісту
- Індикація того, що відбувається перехід
- Скидання меж Suspense при навігації
- Застосування запасного варіанту для серверних помилок та вмісту, що опрацьовується тільки на стороні клієнта
- Усунення неполадок
Опис
<Suspense>
Пропси
children
: Реальний UI, який ви хочете відрендерити. Якщоchildren
затримується під час рендерингу, межа Suspense перемкнеться на рендерfallback
.fallback
: Альтернативний UI, який рендериться замість справжнього UI, якщо той ще не завершив завантаження. Будь-який валідний 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>🌀 Завантаження...</h2>; }
Одночасне відображення всього вмісту
За замовчуванням, усе дерево всередині 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
.
Послідовність буде такою:
- Якщо
Biography
ще не завантажився,BigSpinner
буде показано замість усього вмісту. - Як тільки
Biography
закінчить завантаження,BigSpinner
буде замінено бажаним вмістом. - Якщо
Albums
ще не завантажився,AlbumsGlimmer
буде показано замістьAlbums
і його батьківського компонентаPanel
. - Нарешті, як тільки
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"
. Зверніть увагу, як замість запасного варіанту 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> Пошук альбомів: <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> </> ); }
Запобігання заміни запасним варіантом уже відображеного вмісту
Коли компонент затримується, найближча батьківська межа 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
нова, тому перехід не чекає на неї.
Індикація того, що відбувається перехід
У прикладі зверху, як тільки ви натискаєте на кнопку, відсутній візуальний сигнал того, що відбувається навігація. Щоби додати індикатор, ви можете замінити 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
на стороні клієнта.
Усунення неполадок
Як я можу запобігти заміні UI запасним варіантом під час оновлення
Заміна видимого UI запасним варіантом провокує неприємний користувацький досвід. Це може статися, коли оновлення спричиняє затримку компонента, а найближчий Suspense вже показує вміст користувачу.
Щоби запобігти цьому, відмітьте оновлення не терміновим, використовуючи startTransition
. Під час переходу, React зачекає на завантаження даних, щоб запобігти відображенню небажаного запасного варіанту:
function handleNextPageClick() {
// Якщо це оновлення затримається, уже відображений вміст не буде сховано
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Це допоможе уникнути приховання вже існуючого вмісту. Однак, будь-яка наново відрендерена межа Suspense
, усе ще відображатиме запасний варіант щоби уникнути блокування UI і дозволить користувачу бачити вміст як тільки він стане доступним.
React запобігатиме небажаним запасним варіантам лише під час не термінових оновлень. Він не затримуватиме рендеринг, якщо це результат термінового оновлення. Ви повинні використовувати API, такий як startTransition
або useDeferredValue
.
Якщо у ваш маршрутизатор інтегровано Suspense, він повинен огортати оновлення у startTransition
автоматично.