13. Suspense и ErrorBoundary
<Suspense> и <ErrorBoundary> — инструменты для декларативной обработки асинхронных состояний и ошибок. Solid поддерживает их нативно, без дополнительных библиотек.
<Suspense> — граница ожидания
Заголовок раздела «<Suspense> — граница ожидания»Suspense «перехватывает» асинхронные ресурсы в дочерних компонентах и показывает fallback пока данные загружаются:
import { Suspense } from 'solid-js';import { createResource } from 'solid-js';
function UserPage() { const [user] = createResource(() => fetch('/api/user').then(r => r.json()));
// Этот компонент внутри Suspense — пока user загружается, показывается fallback return <div>{user()!.name}</div>;}
function App() { return ( <Suspense fallback={<div>Загрузка...</div>}> <UserPage /> </Suspense> );}Вложенные Suspense
Заголовок раздела «Вложенные Suspense»Можно создавать иерархию Suspense для разных уровней загрузки:
function Dashboard() { return ( <Suspense fallback={<PageSkeleton />}> {/* Верхний уровень — если что-то не загружено */} <Header />
<Suspense fallback={<StatsSkeleton />}> {/* Секция статистики загружается независимо */} <StatsPanel /> </Suspense>
<Suspense fallback={<FeedSkeleton />}> {/* Лента новостей — своя независимая загрузка */} <NewsFeed /> </Suspense> </Suspense> );}<ErrorBoundary> — граница ошибок
Заголовок раздела «<ErrorBoundary> — граница ошибок»ErrorBoundary перехватывает ошибки внутри дерева компонентов:
import { ErrorBoundary } from 'solid-js';
function App() { return ( <ErrorBoundary fallback={(err, reset) => ( <div class="error-container"> <h2>Что-то пошло не так</h2> <pre>{err.message}</pre> <button onClick={reset}>Попробовать снова</button> </div> )} > <RiskyComponent /> </ErrorBoundary> );}reset — функция, которая сбрасывает границу ошибки и перерендеривает дочерние компоненты.
Suspense + ErrorBoundary вместе
Заголовок раздела «Suspense + ErrorBoundary вместе»Правильный порядок: ErrorBoundary снаружи, Suspense внутри:
function DataSection() { return ( <ErrorBoundary fallback={(err, reset) => ( <ErrorCard error={err} onRetry={reset} /> )} > <Suspense fallback={<Spinner />}> {/* Ошибки в ресурсах всплывут до ErrorBoundary */} <DataComponent /> </Suspense> </ErrorBoundary> );}startTransition — загрузка без мерцания
Заголовок раздела «startTransition — загрузка без мерцания»startTransition позволяет отложить обновление без показа fallback:
import { startTransition } from 'solid-js';
function Navigation() { const [route, setRoute] = createSignal('home');
const navigate = (newRoute: string) => { // Без startTransition — Suspense сразу показывает fallback // С startTransition — UI остаётся на текущем контенте, пока новый загружается startTransition(() => { setRoute(newRoute); }); };
return ( <Suspense fallback={<PageLoader />}> <Show when={route() === 'home'}><HomePage /></Show> <Show when={route() === 'profile'}><ProfilePage /></Show> </Suspense> );}useTransition — с индикатором загрузки
Заголовок раздела «useTransition — с индикатором загрузки»import { useTransition } from 'solid-js';
function TabBar() { const [activeTab, setActiveTab] = createSignal('posts'); const [pending, start] = useTransition();
return ( <div> <nav> <button onClick={() => start(() => setActiveTab('posts'))} // Индикатор пока новая вкладка загружается style={{ opacity: pending() ? 0.7 : 1 }} > Посты {pending() && '...'} </button> <button onClick={() => start(() => setActiveTab('photos'))}> Фото </button> </nav> <Suspense> <Show when={activeTab() === 'posts'}><Posts /></Show> <Show when={activeTab() === 'photos'}><Photos /></Show> </Suspense> </div> );}Паттерн восстановления после ошибки
Заголовок раздела «Паттерн восстановления после ошибки»function SmartErrorBoundary(props: { children: any }) { return ( <ErrorBoundary fallback={(error, reset) => { // Автоматическая повторная попытка через 5 секунд const [countdown, setCountdown] = createSignal(5); const timer = setInterval(() => { setCountdown(c => { if (c <= 1) { clearInterval(timer); reset(); return 0; } return c - 1; }); }, 1000); onCleanup(() => clearInterval(timer));
return ( <div class="error-card"> <p>{error.message}</p> <p>Повтор через {countdown()} сек...</p> <button onClick={() => { clearInterval(timer); reset(); }}> Повторить сейчас </button> </div> ); }} > {props.children} </ErrorBoundary> );}Ошибки в ресурсах vs ошибки в рендере
Заголовок раздела «Ошибки в ресурсах vs ошибки в рендере»// Ошибки createResource всплывают к ErrorBoundaryfunction DataComponent() { const [data] = createResource(async () => { throw new Error('Сервер недоступен'); // → попадёт в ErrorBoundary }); return <div>{data()!.value}</div>;}
// Ошибки в JSX тоже всплывают к ErrorBoundaryfunction BadRender() { const [items] = createResource(fetchItems); // TypeScript не поможет, если items() может быть undefined return <div>{items()!.map(i => i.value)}</div>; // → ReferenceError → ErrorBoundary}