<Suspense> показує fallback, поки щось усередині «не готове»:
- компонент, що читає незавершений проміс через
use(); - лінивий компонент (
lazy(() => import(...))), чий код ще завантажується; - дані фреймворку з підтримкою Suspense (серверні компоненти, завантажувачі Next.js, TanStack Query в режимі
useSuspenseQuery).
<Suspense fallback={<PageSkeleton />}>
<ProfileHeader />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</Suspense>
Як поводяться межі:
- «призупинений» компонент показує fallback найближчої межі вгору по дереву;
- уся межа відображає fallback, поки не готовий хоча б один компонент усередині, - не лише той, що чекає;
- вкладені межі дають поступове відкриття: спершу сторінка зі скелетом постів, потім пости.
Проєктування станів завантаження - питання UX, а не техніки:
- межі за змістовими блоками, а не на кожному компоненті. Двадцять незалежних спінерів, що з'являються в довільному порядку, гірші за один скелет сторінки;
- те, що має з'являтися разом, - в одній межі (заголовок і обкладинка), те, що може відстати, - в окремій (коментарі, рекомендації);
- fallback - скелет того самого розміру, щоб сторінка не стрибала при появі вмісту.
Повторне завантаження без «блимання»: якщо вже показаний вміст знову призупиняється (новий запит при переході між вкладками), межа заховала б готовий вміст за fallback. Щоб цього уникнути, оновлення позначають як transition (startTransition, useTransition) - React залишає старий вміст на екрані, поки готується новий. Також useDeferredValue для значень з введення.
Помилки: відхилений проміс у межі Suspense іде до найближчого error boundary. Зазвичай їх ставлять поруч: межа помилок навколо межі Suspense.
Серверний рендер і стримінг: з renderToPipeableStream/RSC сервер одразу відправляє HTML з fallback-ами, а готові частини дописує в потік. Межі Suspense визначають, які частини сторінки можуть приходити пізніше, - і селективну гідратацію (React гідратує спочатку ту частину, з якою користувач взаємодіє).
Чого Suspense не робить сам по собі: не завантажує дані. Він лише реагує на «призупинення» - джерело даних має його підтримувати (use зі стабільним промісом, фреймворк, бібліотека). useEffect + useState з Suspense не працюють.