Key Takeaway
The difference between server and client components in Next.js: in the App Router every component is a Server Component by default, rendered on the server and shipping no JavaScript of its own. Add use client on the first line of a file only for parts that need state, event handlers or browser APIs, and pass serializable props.
You write useState in a Next.js page, the build fails, and the error tells you to add use client. You do, and the error goes away. A few minutes later you pass an onClick function as a prop from the page to a button component, and a new error appears about props that cannot be serialized. This is where beginners usually get stuck: they know there are two kinds of component but not the rule behind them.
This guide walks you through six small steps in a fresh app, from the default component to the children pattern, hydration and server-only code. Every rule is checked against the official Next.js and React docs. The example in step 5 uses the same pattern as this site's SiteChrome component, and the other facts match the free lesson linked above.
Want to see this site's real code, including the 3D game route that loads its engine only after the visitor agrees? Take the free lesson Learn Server and Client Components in Next.js from Real Code in the Web app skills track, with an interactive explainer and a quiz.
What you need
No large project is required. One empty app is enough to try every step.
- A supported Node.js and a terminal. The commands below use npm.
- A fresh Next.js app with the App Router, created with create-next-app in step 1. Accept the defaults.
- A code editor and a browser. You will read the raw HTML from the server with curl instead of only looking at the inspector.
Step 1: Know the default, every component is a Server Component
Create the app and start it. In the app folder, files such as layout.tsx and page.tsx are Server Components because nothing above them says otherwise. The Next.js docs say layouts and pages are Server Components by default. A Server Component renders on the server and the browser receives only the rendered output, so that code can query a database or use an API key without leaking it.
npx create-next-app@latest lab
cd lab
npm run dev
# app/page.tsx has no "use client" line, so it is a Server Component.
# It may be async, read a database, or use a secret. It may NOT call
# useState or useEffect, and it may NOT have an onClick.There is one price: a Server Component has no state and no event handlers. Server code runs once and its output is sent, and no component stays alive in the browser to hold state. Almost every beginner error comes from this rule, so learn it before moving on.
Step 2: Create your first Client Component with use client
Now create one small file that genuinely needs state: a like button. Add use client on the very first line, before any import. The React docs say the directive must be at the very beginning of a file, above any imports or other code (comments are fine), written with single or double quotes but not backticks. A directive placed below the imports is not recognised.
// app/like-button.tsx
// Wrong: the directive is below the imports, so it is not at the top of the file
import { useState } from "react";
"use client";
// Right: the very first line (comments are fine above it), before any import
"use client";
import { useState } from "react";
export default function LikeButton({ start }: { start: number }) {
const [likes, setLikes] = useState(start);
return <button onClick={() => setLikes(likes + 1)}>{likes} likes</button>;
}Note that use client applies to the whole module and cannot go inside a function or component. You only need it on files whose components a Server Component renders directly. Other files that file imports join the browser bundle without needing a directive of their own.
Step 3: Use it from a server page and pass safe props
The page stays a Server Component. It imports the button and passes a plain number as a prop. Then run curl to see the HTML the server sent. The page heading is already inside, even though the button is a Client Component. That is because on the first load Client Components are also prerendered to HTML on the server, then JavaScript hydrates them to attach event handlers. A Client Component does not mean browser-only.
// app/page.tsx (Server Component: no directive needed)
import LikeButton from "./like-button";
export default function Page() {
return (
<main>
<h1>Server text, rendered once</h1>
{/* Right: a number is serializable */}
<LikeButton start={3} />
{/* Wrong: a function cannot cross the server-to-client boundary
<LikeButton start={3} onDone={() => {}} /> */}
</main>
);
}# Check the HTML the server sent, not the inspector:
curl -s http://localhost:3000 | grep -o 'Server text, rendered once'Props that cross the use client boundary must be serializable so React can send them from server to browser. According to the React docs that includes strings, numbers, booleans, null, Date, arrays, Map, Set, plain objects, JSX elements and Promises. Ordinary functions and class instances are not allowed. That is why this site's PlayShell takes a locale string and a number, not a callback such as onClose.
Do not fix a props error by putting use client on the page. That only moves the whole page and all of its imports into the browser bundle. Move the handler inside the Client Component and send only plain data from the server.
Step 4: Put the use client boundary as deep as you can
The use client directive pulls everything that file imports into the client bundle, so the higher you put it, the more JavaScript you ship. The Next.js docs give an example: a layout with a static logo and navigation links, plus one interactive search bar. Only the search bar needs to be a Client Component. The logo and the layout stay on the server.
// app/layout.tsx stays a Server Component
import Search from "./search"; // "use client" lives inside search.tsx
import Logo from "./logo"; // plain Server Component, ships no JS of its own
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
// Wrong: "use client" on layout.tsx would pull Logo and every import
// of the layout into the browser bundle for no reason.A good habit: start with Server Components for everything, then move interactive pieces into their own small files one at a time. Do not reverse the order by marking the page as a client and trying to repair it later.
Step 5: Mix both with the children pattern
Sometimes an interactive part needs to wrap content rendered on the server, such as a modal that opens and closes with state but whose content is a cart loaded from a database. Pass the Server Component as children to the Client Component. The Client Component only provides a slot. According to the Next.js docs, a component passed as children or another prop is not imported into the client module graph, is rendered on the server ahead of time, and reaches the Client Component as rendered output.
// app/ui/modal.tsx: the client part only owns the open/closed state
"use client";
import { useState } from "react";
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <div>{children}</div>}
</div>
);
}
// app/page.tsx: Cart stays a Server Component, rendered ahead of time
import Modal from "./ui/modal";
import Cart from "./ui/cart";
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}This site's SiteChrome component uses the same pattern. It must read the pathname, so it is a Client Component, but the header and footer are rendered in the server layout and passed in as ReactNode props. SiteChrome only decides whether to show them.
// app/components/SiteChrome.tsx (this site): a Client Component whose
// props are ReactNode slots filled in by the server layout
interface SiteChromeProps {
header: ReactNode;
footer: ReactNode;
/** Floating helpers: the chatbot, the selection explainer. */
extras: ReactNode;
children: ReactNode;
}The same applies to providers such as a theme. React context is not supported in Server Components, so create a small provider with use client that accepts children, then mount it in the server layout. The docs advise rendering providers as deep as possible: wrap children, not the whole html element.
Step 6: Avoid hydration errors and leaking server code
Because a Client Component renders twice, once on the server and once in the browser during hydration, both results must match. The Next.js docs list the common causes: using Date, window or localStorage directly in render logic, checking typeof window, and wrongly nested HTML such as a p inside a p. The fix is to render the same thing on both sides, then fill in browser-only values in useEffect.
"use client";
import { useEffect, useState } from "react";
// Wrong: the server renders one thing, the browser's first render another,
// so React reports a hydration mismatch
export function ClockWrong() {
return <p>{new Date().toLocaleTimeString()}</p>;
}
// Right: render the same placeholder on both sides, fill in after hydration
export function ClockRight() {
const [now, setNow] = useState("");
useEffect(() => {
setNow(new Date().toLocaleTimeString());
}, []);
return <p>{now || "..."}</p>;
}For a full worked example and how to read the error, see the guide to fixing Next.js hydration errors. The docs give two other ways out: load the component with next/dynamic and ssr false inside a Client Component, or use suppressHydrationWarning on a single element such as a timestamp. The second is an escape hatch, not something to overuse.
When to use which: a decision table
Ask one question about each component: does it need something that exists only in the browser? If not, leave it as a Server Component. The list below follows the guidance in the Next.js docs.
| You need | Choose | Short reason |
|---|---|---|
| useState, onClick, onChange | Client Component | A Server Component cannot hold state or receive events. |
| useEffect or a custom hook | Client Component | Lifecycle logic runs in the browser. |
| window, localStorage, navigator.geolocation | Client Component | Browser-only APIs do not exist on the server. |
| Fetching data from a database or API | Server Component | Close to the data source, and the result adds no JavaScript. |
| API keys, tokens and other secrets | Server Component | They are never sent to the browser. |
| Article text and static content | Server Component | Less JavaScript, and the text is in the first HTML. |
Common errors and how to fix them
These six symptoms stop beginners most often. Match the message you see against the first column.
| Symptom | Cause | Fix |
|---|---|---|
| An error saying a React hook only works in a Client Component | A file uses useState or useEffect but has no use client, so it counts as a Server Component. | Move the interactive part into a small file with use client on the first line. The page stays on the server. |
| Props reported as not serializable | A function was passed from a Server Component to a Client Component. | Pass plain data and define the handler inside the Client Component. |
| Hydration error: Text content does not match server-rendered HTML | The server output differs from the first browser render, for example because of Date, window or wrongly nested HTML. | Render the same value on both sides, then fill in browser-only values in useEffect. |
| An error that ssr false is not allowed with next/dynamic in a Server Component | The ssr false option is only supported in Client Components. | Move the dynamic import into a file that starts with use client. |
| A data call fails quietly in the browser | A helper that reads a secret was imported into a Client Component. Variables without the NEXT_PUBLIC_ prefix become an empty string in the browser. | Add an import of server-only at the top of the helper so you get a build error instead. |
| A third-party library component errors in a Server Component | The library uses state but its module has no use client. | Wrap it in a small file of your own that starts with use client, then import the wrapper. |
// lib/data.ts: fails the build if a Client Component ever imports it
import "server-only";
export async function getData() {
const res = await fetch("https://api.example.com/data", {
headers: { authorization: process.env.API_KEY ?? "" },
});
return res.json();
}
// Wrong: next/dynamic with ssr: false inside a Server Component
// Right: put that dynamic import inside a file that starts with "use client"Checklist before you move on
- Pages and layouts stay Server Components, with no use client above them.
- use client is on the first line, only in small files that really need state, effects or browser APIs.
- Props that cross the boundary hold plain data, with no functions.
- Server content that an interactive component must wrap is passed through children.
- Code that reads secrets imports server-only, and browser values such as Date or window are moved into useEffect.
Learn more
This guide focuses on practical steps. For the full picture, including how this site's game route holds back the 3D engine until the visitor agrees and why page text must never sit behind a loading skeleton, open the lesson Learn Server and Client Components in Next.js from Real Code. When you start using forms and server-side mutations, also read Next.js Server Actions security, because the server and client boundary decides what is safe there too.
A rule you can carry: start from Server Components, add use client only to the smallest piece that needs the browser, and send across that boundary only data that can be serialized.

