Suspense

A component that displays fallback content while its children are loading.

Syntax

<Suspense fallback={<Loading />}>{children}</Suspense>
<Suspense fallback={<Loading />} timeout={500}>{children}</Suspense>

Props

PropTypeDefaultDescription
childrenChildrenrequiredThe content to display when loading is complete
fallbackChildrenrequiredThe content to display while children are loading
timeoutnumber300Time in milliseconds before showing fallback

Description

Suspense provides a way to display fallback content (like a loading spinner) while waiting for async children to resolve. It's commonly used with:

The timeout prop controls how long Suspense waits before showing the fallback. This prevents flash of loading content for fast operations.

When used within a SuspenseList, Suspense coordinates with siblings to control reveal order and fallback behavior, inheriting the timeout from the parent SuspenseList if not specified.

Examples

Basic usage

import {Suspense} from "@b9g/crank/async";

async function UserProfile({id}) {
const user = await fetchUser(id);
return (
<div class="profile">
<img src={user.avatar} />
<h2>{user.name}</h2>
</div>
);
}

function App() {
return (
<Suspense fallback={<div class="skeleton">Loading...</div>}>
<UserProfile id={123} />
</Suspense>
);
}

With lazy components

import {lazy, Suspense} from "@b9g/crank/async";

const HeavyEditor = lazy(() => import("./HeavyEditor"));

function* App() {
let showEditor = false;

for (const props of this) {
yield (
<div>
<button onclick={() => { showEditor = true; this.refresh(); }}>
Open Editor
</button>

{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<HeavyEditor />
</Suspense>
)}
</div>
);
}
}

Custom timeout

import {Suspense} from "@b9g/crank/async";

function App() {
return (
<div>
{/* Show fallback immediately */}
<Suspense timeout={0} fallback={<Spinner />}>
<SlowComponent />
</Suspense>

{/* Wait 500ms before showing fallback */}
<Suspense timeout={500} fallback={<Spinner />}>
<FastComponent />
</Suspense>
</div>
);
}

Nested Suspense boundaries

import {Suspense} from "@b9g/crank/async";

function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
</Suspense>
);
}

Error boundaries

import {Suspense} from "@b9g/crank/async";

function* ErrorBoundary({children}) {
try {
for (const {children} of this) {
yield children;
}
} catch (error) {
yield <div class="error">Something went wrong: {error.message}</div>;
}
}

function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}

Loading states with transitions

import {Suspense} from "@b9g/crank/async";

function* TabPanel() {
let activeTab = "overview";

for (const props of this) {
yield (
<div>
<nav>
<button onclick={() => { activeTab = "overview"; this.refresh(); }}>
Overview
</button>
<button onclick={() => { activeTab = "details"; this.refresh(); }}>
Details
</button>
</nav>

<Suspense
fallback={<TabSkeleton />}
timeout={200}
>
{activeTab === "overview" && <Overview />}
{activeTab === "details" && <Details />}
</Suspense>
</div>
);
}
}

See also

Edit on GitHub