Skip to Content
ExamplesNext.js Server Components

Next.js Server Components

A product catalog built with Next.js App Router that demonstrates the React Server Component model. Server components fetch data directly with async/await — no useEffect, no loading spinners, no API layer. A client component adds interactivity (search), and Suspense streams a slow server component so the rest of the page isn’t blocked.

How it works

The page is an async server component. It awaits getProducts() and passes the result as props to a client ProductSearch component, which adds filtering. A separate Stats server component runs its own slow query — it’s wrapped in Suspense so the page renders immediately and the stats stream in when ready. The boundary between server and client is the 'use client' directive at the top of product-search.tsx.

nextjs-server-components/ ├── lib/db.ts ← simulated async database (two queries, one slow) ├── app/layout.tsx ← root layout (minimal) ├── app/page.tsx ← async server component: fetches data, composes page ├── app/stats.tsx ← async server component: slow query, streamed via Suspense └── app/product-search.tsx ← 'use client': receives products as props, adds search

lib/db.ts

Simulated database with two async functions. getProducts returns quickly. getStats has an artificial 2-second delay to demonstrate Suspense streaming — in a real app this would be a slow aggregation query.

lib/db.ts

app/layout.tsx

Minimal root layout — just the HTML shell and the system font stack.

import type { ReactNode } from 'react' export const metadata = { title: 'Next.js Server Components' } export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 560, margin: '2rem auto' }}> {children} </body> </html> ) }

app/page.tsx

This is an async server component — the default in Next.js App Router. It calls getProducts() directly (no fetch, no API route) and passes the result as props to the client ProductSearch. The Stats component is another async server component wrapped in Suspense — it won’t block the page from rendering. When the slow query finishes, React streams the HTML in and replaces the fallback.

app/page.tsx

app/stats.tsx

An async server component that runs its own data fetch. Because it’s wrapped in Suspense in the parent, the rest of the page renders immediately — the browser shows “Loading stats…” and then this component streams in once getStats() resolves. No client JavaScript involved.

app/stats.tsx

app/product-search.tsx

'use client' marks the server/client boundary. Everything in this file runs in the browser. The products prop was fetched on the server and passed down — the client component never calls an API or runs a query. It just adds interactivity: a search input that filters the list by name or category. This is the core pattern — server components fetch, client components interact. Note the import type — type-only imports are erased at compile time so they’re safe to use from client components without pulling server code into the bundle.

app/product-search.tsx

Run it

npx create-next-app@latest nextjs-server-components --ts --app --no-tailwind --no-eslint --no-src-dir cd nextjs-server-components mkdir lib # create the files above npm run dev

Open http://localhost:3000 . The product list appears immediately while “Loading stats…” shows at the top. After two seconds the stats stream in. Type in the search box to filter — that runs entirely in the browser, no server round-trip.

Last updated on