Пошук уроків, статей та іншого контенту
Побудуємо стани завантаження для асинхронних компонентів за допомогою Suspense та fallback-інтерфейсів.
Suspense — це компонент React, який показує запасний інтерфейс, доки його дочірній компонент не буде готовий до відображення.
Найпоширеніший сценарій — відкладене завантаження компонентів за допомогою React.lazy. Код великого або рідко використовуваного компонента не потрапляє в основний JavaScript-файл, а завантажується окремим фрагментом лише тоді, коли цей компонент потрібен.
import { Suspense, lazy } from 'react';
const SettingsPage = lazy(() => import('./SettingsPage.jsx'));
function App() {
return (
<main>
<h1>Налаштування</h1>
<Suspense fallback={<p>Завантаження налаштувань...</p>}>
<SettingsPage />
</Suspense>
</main>
);
}
export default App;Поки SettingsPage.jsx завантажується, React відображає:
Завантаження налаштувань...Після завершення завантаження запасний інтерфейс автоматично замінюється компонентом SettingsPage.
lazy приймає функцію, яка повертає динамічний імпорт:
const Component = lazy(() => import('./Component.jsx'));Функція імпорту повинна повертати модуль із default-експортом компонента:
function SettingsPage() {
return <section>Сторінка налаштувань</section>;
}
export default SettingsPage;Якщо компонент експортується іменовано, його потрібно перетворити на default-експорт у функції імпорту:
import { lazy } from 'react';
const ReportsPage = lazy(() =>
import('./ReportsPage.jsx').then((module) => ({
default: module.ReportsPage,
})),
);Файл ReportsPage.jsx у такому разі може мати іменований експорт:
export function ReportsPage() {
return <section>Звіти</section>;
}Зазвичай простіше використовувати default-експорт для компонентів, які завантажуються через lazy.
Suspense має обов’язкову властивість fallback. Вона приймає будь-який React-елемент:
<Suspense fallback={<p>Завантаження...</p>}>
<SettingsPage />
</Suspense>fallback може бути як простим текстом, так і повноцінним компонентом:
function LoadingState() {
return (
<div role="status" aria-live="polite">
Завантаження сторінки...
</div>
);
}
function App() {
return (
<Suspense fallback={<LoadingState />}>
<SettingsPage />
</Suspense>
);
}Зручно створювати окремі компоненти стану завантаження, якщо інтерфейс має бути однаковим у різних частинах застосунку.
Нижче наведено приклад із двома асинхронними компонентами. Сторінка звітів завантажується лише після натискання кнопки.
App.jsximport { lazy, Suspense, useState } from 'react';
const ReportsPage = lazy(() => import('./ReportsPage.jsx'));
function LoadingState({ text = 'Завантаження...' }) {
return (
<p role="status" aria-live="polite">
{text}
</p>
);
}
function App() {
const [showReports, setShowReports] = useState(false);
return (
<main>
<h1>Панель керування</h1>
<p>Основний вміст уже доступний користувачу.</p>
<button type="button" onClick={() => setShowReports(true)}>
Відкрити звіти
</button>
{showReports && (
<section>
<h2>Звіти</h2>
<Suspense fallback={<LoadingState text="Завантаження звітів..." />}>
<ReportsPage />
</Suspense>
</section>
)}
</main>
);
}
export default App;ReportsPage.jsxconst reports = [
{ id: 1, title: 'Продажі за січень', value: '12 450 грн' },
{ id: 2, title: 'Продажі за лютий', value: '15 800 грн' },
];
function ReportsPage() {
return (
<ul>
{reports.map((report) => (
<li key={report.id}>
{report.title}: {report.value}
</li>
))}
</ul>
);
}
export default ReportsPage;У цьому прикладі:
ReportsPage не завантажується під час першого відображення App.
Після натискання кнопки React починає завантаження модуля.
Поки модуль завантажується, відображається LoadingState.
Після завантаження React показує список звітів.
При наступному відкритті вже завантажений модуль зазвичай береться з кешу.
Suspense працює для всіх дочірніх компонентів усередині своєї межі. Якщо будь-який із них призупиняє відображення, React показує fallback цієї межі.
<Suspense fallback={<p>Завантаження всього розділу...</p>}>
<Header />
<ReportsPage />
<Sidebar />
</Suspense>У такому варіанті запасний інтерфейс замінить усі три дочірні компоненти на час завантаження ReportsPage.
Іноді краще використовувати кілька незалежних меж:
function Dashboard() {
return (
<div>
<Suspense fallback={<p>Завантаження звітів...</p>}>
<ReportsPage />
</Suspense>
<Suspense fallback={<p>Завантаження профілю...</p>}>
<UserProfile />
</Suspense>
</div>
);
}Тоді завантаження одного розділу не приховує інший. Профіль може з’явитися раніше за звіти, якщо його модуль завантажиться швидше.
Розташовуйте Suspense навколо логічної частини інтерфейсу:
для всієї сторінки — якщо без компонента немає сенсу показувати сторінку;
для окремої панелі — якщо інші панелі можуть працювати незалежно;
для великого списку — якщо список має власний стан завантаження.
Занадто велика межа може приховати вже готовий інтерфейс. Занадто дрібні межі можуть створити багато окремих індикаторів завантаження.
Suspense не перехоплює звичайний асинхронний запит, виконаний у useEffect.
Цей код самостійно керує станом завантаження:
import { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadUsers() {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
setIsLoading(false);
}
loadUsers();
}, []);
if (isLoading) {
return <p>Завантаження користувачів...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Suspense не замінює isLoading у цьому прикладі. Компонент повертає стан завантаження самостійно, а не призупиняє відображення для Suspense.
Застосовуйте Suspense для завдань, які його підтримують, зокрема:
відкладеного завантаження компонентів через lazy;
інших інтеграцій і джерел даних, які спеціально підтримують Suspense.
Не потрібно обгортати кожен fetch у Suspense в надії, що він автоматично покаже fallback.
Suspense показує стан очікування, але не є обробником помилок. Якщо динамічний імпорт завершиться помилкою, для її відображення потрібен Error Boundary.
Для простих прикладів помилки завантаження можна обробляти на рівні застосунку за допомогою Error Boundary. Межа помилок — це компонент класу, який реалізує getDerivedStateFromError і componentDidCatch.
import { Component, lazy, Suspense } from 'react';
const ReportsPage = lazy(() => import('./ReportsPage.jsx'));
class LoadingErrorBoundary extends Component {
state = {
hasError: false,
};
static getDerivedStateFromError() {
return {
hasError: true,
};
}
render() {
if (this.state.hasError) {
return <p>Не вдалося завантажити розділ.</p>;
}
return this.props.children;
}
}
function App() {
return (
<LoadingErrorBoundary>
<Suspense fallback={<p>Завантаження...</p>}>
<ReportsPage />
</Suspense>
</LoadingErrorBoundary>
);
}
export default App;У цьому порядку:
Suspense відповідає за очікування;
LoadingErrorBoundary відповідає за помилку;
кожен компонент виконує лише одну відповідальність.
Suspense без fallback<Suspense>
<ReportsPage />
</Suspense>fallback потрібно вказати явно:
<Suspense fallback={<p>Завантаження...</p>}>
<ReportsPage />
</Suspense>lazy використовується поза Suspenseconst ReportsPage = lazy(() => import('./ReportsPage.jsx'));
function App() {
return <ReportsPage />;
}Компонент, завантажений через lazy, має бути дочірнім елементом Suspense:
function App() {
return (
<Suspense fallback={<p>Завантаження...</p>}>
<ReportsPage />
</Suspense>
);
}default-експортуЦей варіант не відповідає типовому очікуванню lazy:
export function ReportsPage() {
return <p>Звіти</p>;
}Потрібен default-експорт:
export default function ReportsPage() {
return <p>Звіти</p>;
}lazy під час кожного рендерингуНе створюйте lazy-компонент усередині іншого компонента:
function App() {
const ReportsPage = lazy(() => import('./ReportsPage.jsx'));
return (
<Suspense fallback={<p>Завантаження...</p>}>
<ReportsPage />
</Suspense>
);
}Оголошуйте його на рівні модуля:
const ReportsPage = lazy(() => import('./ReportsPage.jsx'));
function App() {
return (
<Suspense fallback={<p>Завантаження...</p>}>
<ReportsPage />
</Suspense>
);
}Так React працює зі стабільним типом компонента між рендерами.
fallbackЯкщо одна межа охоплює весь застосунок, коротке завантаження маленької панелі може приховати весь інтерфейс:
<Suspense fallback={<p>Завантаження всього застосунку...</p>}>
<Header />
<Sidebar />
<ReportsPage />
</Suspense>Якщо частини сторінки незалежні, створюйте окремі межі для них.
Для повідомлення про тривале завантаження можна додати семантику статусу:
function LoadingState() {
return (
<p role="status" aria-live="polite">
Завантаження...
</p>
);
}Так допоміжні технології зможуть повідомити користувачу про зміну стану.
Suspense відображає fallback, доки дочірній компонент не буде готовий.
React.lazy дає змогу завантажувати компоненти окремими фрагментами.
Компонент із lazy потрібно розміщувати всередині Suspense.
Компонент, який імпортується через lazy, зазвичай має default-експорт.
Кілька меж Suspense дають змогу завантажувати незалежні частини інтерфейсу окремо.
Suspense і fallback не замінюють автоматично логіку isLoading для запитів у useEffect.
Для помилок завантаження потрібен окремий Error Boundary.
Розмір межі потрібно добирати відповідно до структури інтерфейсу.