javascript 54 lines · 8 steps

Streaming a product list with Suspense in Next.js

An async Server Component fetches data while Suspense streams a skeleton placeholder until it's ready.

Explained by highlit
1import { Suspense } from 'react';
2import { getProducts } from '@/lib/api/products';
3import { formatPrice } from '@/lib/format';
4 
5export default function ProductsPage({ searchParams }) {
6 const category = searchParams?.category ?? 'all';
7 
8 return (
9 <section className="mx-auto max-w-6xl px-4 py-8">
10 <h1 className="mb-6 text-2xl font-semibold">Products</h1>
11 <Suspense key={category} fallback={<ProductGridSkeleton />}>
12 <ProductGrid category={category} />
13 </Suspense>
14 </section>
15 );
16}
17 
18async function ProductGrid({ category }) {
19 const products = await getProducts({ category });
20 
21 if (products.length === 0) {
22 return <p className="text-sm text-gray-500">No products found.</p>;
23 }
24 
25 return (
26 <ul className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
27 {products.map((product) => (
28 <li key={product.id} className="rounded-lg border p-4">
29 <img
30 src={product.imageUrl}
31 alt={product.name}
32 className="aspect-square w-full rounded-md object-cover"
33 />
34 <h2 className="mt-3 truncate font-medium">{product.name}</h2>
35 <p className="text-sm text-gray-600">{formatPrice(product.price)}</p>
36 </li>
37 ))}
38 </ul>
39 );
40}
41 
42function ProductGridSkeleton() {
43 return (
44 <ul className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
45 {Array.from({ length: 8 }).map((_, i) => (
46 <li key={i} className="rounded-lg border p-4">
47 <div className="aspect-square w-full animate-pulse rounded-md bg-gray-200" />
48 <div className="mt-3 h-4 w-3/4 animate-pulse rounded bg-gray-200" />
49 <div className="mt-2 h-4 w-1/3 animate-pulse rounded bg-gray-200" />
50 </li>
51 ))}
52 </ul>
53 );
54}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Async Server Components let you await data directly inside the component instead of using effects or client-side fetching.
  2. 2Wrapping an async child in Suspense streams a fallback immediately and swaps in the real UI once the data resolves.
  3. 3Keying Suspense on a changing prop forces a fresh fallback whenever that input changes, keeping loading states accurate.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a product list with Suspense in Next.js — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code