How it works
The explanation, with examples you can run.
What a Server Component can do
import { readFile } from "node:fs/promises"import { marked } from "marked"import LikeButton from "./LikeButton" export default async function Article({ slug }: { slug: string }) { const source = await readFile(`./articles/${slug}.md`, "utf8") const html = await marked(source) return ( <article> <div dangerouslySetInnerHTML={{ __html: html }} /> <LikeButton slug={slug} /> </article> )}The component awaits its data during render: no effect, no loading state in the browser, no API endpoint to write. The Markdown library runs on the server and adds nothing to the JavaScript bundle. (Server-rendered Markdown still needs the same care about untrusted input as anywhere else.)
What it can't do: hold state, run effects, or attach event handlers. It renders once per request or build and is finished; there is nothing in the browser to keep state for. Context, useState, useEffect and onClick belong in client components.
The client boundary
"use client" import { useState } from "react" export default function LikeButton({ slug }: { slug: string }) { const [liked, setLiked] = useState(false) return ( <button onClick={() => setLiked(!liked)} data-slug={slug}> {liked ? "Liked" : "Like"} </button> )}"use client" at the top of a file marks a boundary in the module graph. That module and everything it imports are bundled for the browser. A Server Component can import and render it; the client component is server-rendered to HTML too, then hydrated in the browser so its handlers work. "Client component" means "also runs in the browser", not "runs only in the browser".
There is no directive for Server Components; in an app set up for them, components are server components unless they come from a client module. "use server" is something else: it marks Server Functions, async functions on the server that client code can call, for example as a form action.
What crosses the boundary
When a Server Component renders a client component, the props are serialised and sent with the result. Allowed: primitives, plain objects and arrays of serialisable values, Date, Map, Set, typed arrays, promises, JSX elements, and Server Functions. Not allowed: ordinary functions (such as event handlers), class instances, and symbols that aren't registered globally.
Passing JSX lets a client component wrap server-rendered content: <Tabs>{serverRenderedPanel}</Tabs> keeps the panel's code on the server even though Tabs is interactive.
Placing the line
- Keep the boundary low. Mark the small interactive leaves (a button, a search box, a menu) as client components, rather than a whole page.
- Fetch in Server Components, close to the data, and pass the results down as props.
- Keep secrets in server-only modules; anything imported by a client module ends up in the browser bundle.
- Wrap slow server parts in
<Suspense>so the rest of the page can be streamed first.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.