Перейти к содержимому

13. Suspense и ErrorBoundary

<Suspense> и <ErrorBoundary> — инструменты для декларативной обработки асинхронных состояний и ошибок. Solid поддерживает их нативно, без дополнительных библиотек.

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 для разных уровней загрузки:

function Dashboard() {
return (
<Suspense fallback={<PageSkeleton />}>
{/* Верхний уровень — если что-то не загружено */}
<Header />
<Suspense fallback={<StatsSkeleton />}>
{/* Секция статистики загружается независимо */}
<StatsPanel />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
{/* Лента новостей — своя независимая загрузка */}
<NewsFeed />
</Suspense>
</Suspense>
);
}

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 — функция, которая сбрасывает границу ошибки и перерендеривает дочерние компоненты.

Правильный порядок: ErrorBoundary снаружи, Suspense внутри:

function DataSection() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<ErrorCard error={err} onRetry={reset} />
)}
>
<Suspense fallback={<Spinner />}>
{/* Ошибки в ресурсах всплывут до ErrorBoundary */}
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}

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>
);
}
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>
);
}
// Ошибки createResource всплывают к ErrorBoundary
function DataComponent() {
const [data] = createResource(async () => {
throw new Error('Сервер недоступен'); // → попадёт в ErrorBoundary
});
return <div>{data()!.value}</div>;
}
// Ошибки в JSX тоже всплывают к ErrorBoundary
function BadRender() {
const [items] = createResource(fetchItems);
// TypeScript не поможет, если items() может быть undefined
return <div>{items()!.map(i => i.value)}</div>; // → ReferenceError → ErrorBoundary
}