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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Async Server Components let you await data directly inside the component instead of using effects or client-side fetching.
- 2Wrapping an async child in Suspense streams a fallback immediately and swaps in the real UI once the data resolves.
- 3Keying Suspense on a changing prop forces a fresh fallback whenever that input changes, keeping loading states accurate.
Related explainers
javascript
import Link from "next/link"; import { prisma } from "@/lib/prisma"; const PAGE_SIZE = 20;
A searchable, paginated table as a Next.js Server Component
server-components
pagination
search
Intermediate
6 steps
python
import ijson from decimal import Decimal
Streaming JSON aggregation with ijson
streaming
json-parsing
generators
Intermediate
7 steps
javascript
const DEFAULT_OPTIONS = { method: 'GET', timeout: 5000, retries: 3,
A resilient fetch wrapper with retries and timeout
fetch
retry
timeout
Intermediate
8 steps
javascript
import { parsePhoneNumberFromString } from 'libphonenumber-js'; export class PhoneValidationError extends Error { constructor(message, code) {
Normalizing phone numbers with typed errors
validation
error-handling
custom-errors
Intermediate
6 steps
javascript
const express = require('express'); const { z } = require('zod'); const router = express.Router();
Validating query params with Zod in Express
validation
schema-parsing
query-building
Intermediate
9 steps
javascript
const RESERVED_SLUGS = new Set(['new', 'edit', 'admin', 'api']); function slugify(title) { return title
Generating URL-safe unique slugs
string-normalization
regex
deduplication
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-a-product-list-with-suspense-in-next-js-explained-javascript-9467/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.