<Suspense>
<Suspense>
дає вам змогу відображати запасний варіант (fallback), доки його дочірні компоненти не завершать завантаження.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Опис
- Використання
- Відображення запасного варіанту під час завантаження вмісту
- Одночасне відображення всього вмісту
- Відображення вкладеного вмісту поступово, відповідно до його завантаження
- Відображення застарілого вмісту під час завантаження нового
- Запобігання заміни запасним варіантом уже відображеного вмісту
- Індикація переходу
- Скидання меж 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>; }
Одночасне відображення всього вмісту
Початково, усе дерево всередині 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"
. Зверніть увагу, як замість запасного варіанту, ви бачите затемнений список попередніх результатів, доки нові результати не завантажилися:
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('Чат повинен рендеритися тільки на стороні клієнта.');
}
// ...
}
Відрендерений на стороні сервера HTML включатиме лише індикатор завантаження. Його буде замінено компонентом Chat
на стороні клієнта.
Усунення неполадок
Як я можу запобігти заміні UI запасним варіантом під час оновлення?
Заміна видимого UI запасним варіантом спричиняє неприємний користувацький досвід. Це стається, коли оновлення спричиняє затримку компонента, а найближчий Suspense вже показує вміст користувачу.
Щоб запобігти цьому, відмітьте оновлення нетерміновим, використовуючи startTransition
. Під час переходу, React зачекає на завантаження даних, щоб запобігти відображенню небажаного запасного варіанту:
function handleNextPageClick() {
// Якщо це оновлення затримається, уже відображений вміст не буде сховано
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Це допоможе уникнути приховання вже існуючого вмісту. Однак, будь-яка наново відрендерена межа Suspense
, усе ще відображатиме запасний варіант, щоб уникнути блокування UI і дасть змогу користувачу бачити вміст як тільки він стане доступним.
React запобігатиме небажаним запасним варіантам лише під час нетермінових оновлень. Він не затримуватиме рендеринг, якщо це результат термінового оновлення. Ви повинні увімкнути це з API, наприклад, startTransition
або useDeferredValue
.
Якщо у ваш маршрутизатор інтегровано Suspense, він повинен огортати оновлення у startTransition
автоматично.