SuspenseList

Controls when child Suspense components show their content or fallbacks based on a specified reveal order.

Syntax

<SuspenseList revealOrder="forwards">{children}</SuspenseList>
<SuspenseList revealOrder="together" tail="collapsed">{children}</SuspenseList>
<SuspenseList revealOrder="backwards" timeout={100}>{children}</SuspenseList>

Props

PropTypeDefaultDescription
childrenChildrenrequiredElements containing Suspense components to coordinate
revealOrderstring"forwards"How children should be revealed
tailstring"collapsed"How to handle fallbacks
timeoutnumber-Default timeout for Suspense children

revealOrder values

tail values

Description

SuspenseList coordinates the reveal order of multiple Suspense boundaries. This is useful for:

Suspense components that are not rendered immediately (because they are children of another async component) will not be coordinated.

In Crank, async components by default render together. The "together" mode might not be necessary unless you're using Suspense fallbacks.

Examples

Sequential reveal (forwards)

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

function Feed() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<PostSkeleton />}>
<Post id={1} />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<Post id={2} />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<Post id={3} />
</Suspense>
</SuspenseList>
);
}

Post 1 will appear first, then Post 2 (even if it loads faster), then Post 3.

Collapsed tail

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

function SearchResults({results}) {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
{results.map((result) => (
<Suspense key={result.id} fallback={<ResultSkeleton />}>
<SearchResult data={result} />
</Suspense>
))}
</SuspenseList>
);
}

Only shows one loading skeleton at a time for the next unloaded item.

Hidden fallbacks

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

function Gallery({images}) {
return (
<SuspenseList revealOrder="forwards" tail="hidden">
{images.map((img) => (
<Suspense key={img.id} fallback={null}>
<Image src={img.url} />
</Suspense>
))}
</SuspenseList>
);
}

Images appear one by one with no loading indicators.

Reveal together

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

function Dashboard() {
return (
<SuspenseList revealOrder="together">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<UsersChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<TrafficChart />
</Suspense>
</SuspenseList>
);
}

All charts appear simultaneously when all have loaded.

Backwards reveal

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

function Comments({comments}) {
return (
<SuspenseList revealOrder="backwards">
{comments.map((comment) => (
<Suspense key={comment.id} fallback={<CommentSkeleton />}>
<Comment data={comment} />
</Suspense>
))}
</SuspenseList>
);
}

Newer comments (at the end) appear first.

With shared timeout

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

function Page() {
return (
<SuspenseList revealOrder="forwards" timeout={100}>
{/* All children inherit 100ms timeout */}
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</SuspenseList>
);
}

Nested SuspenseLists

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

function App() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<NavSkeleton />}>
<Navigation />
</Suspense>

<SuspenseList revealOrder="together">
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<Main />
</Suspense>
</SuspenseList>

<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</SuspenseList>
);
}

Navigation shows first, then Sidebar and Main appear together, then Footer.

See also

Edit on GitHub