Server and client components
The one decision you keep making in Next.js
Everything runs on the server until you write "use client". From there it runs in the browser – and lands in the JavaScript your users download.
This is the heart of Next.js and where most error messages come from. The good news: there is exactly one question you have to answer.
Picture a newspaper. Most of it is printed – finished, unchangeable, cheap to distribute: those are Server componentA component that only runs on the server. It may fetch data directly but cannot handle clicks or use hooks.In the App Router this is the default – you do not have to do anything for it.→. A few spots fold out or have a button. Only those need machinery, and the machinery has to ship with it: those are Client componentA component that runs in the browser and can therefore use state, hooks and clicks. Marked with `"use client"` at the top of the file.→.
The one question
Does this piece need …
• useState, useEffect or any other hook?
• onClick, onChange, onSubmit?
• window, localStorage, browser APIs?
YES → "use client"
NO → leave it (runs on the server) // Server component: no "use client" needed
import { AddToCartButton } from "./AddToCartButton";
export default async function Page() {
const products = await db.product.findMany(); // straight to the database
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}
<AddToCartButton id={p.id} /> {/* the button is interactive */}
</li>
))}
</ul>
);
} "use client";
import { useState } from "react";
export function AddToCartButton({ id }: { id: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "In the cart" : "Add to cart"}
</button>
);
} “use client” applies downwards
The line marks a Client boundaryThe point where a server component pulls in a client component. From there on everything ends up in the browser bundle.→. Everything this file imports becomes a client component too – even without its own "use client". Hence the rule: put it as deep as possible.
"use client"; // at the top of the page
export default function Page() {
const [open, setOpen] = useState(false);
return (
<>
<HugeProductList /> {/* now client too */}
<button onClick={() => setOpen(true)}>Filter</button>
</>
);
} // the page stays a server component
export default async function Page() {
const products = await db.product.findMany();
return (
<>
<HugeProductList products={products} />
<FilterButton /> {/* only this file has "use client" */}
</>
);
} A client component may contain server components as children. <ClientTabs><ServerList /></ClientTabs> works: the list is produced on the server and only its finished result is passed through. The one condition is that the client component must not import it directly.
What server components cannot do
- +
Fetch data, hit the database, read files, use secrets
- +
Use large dependencies without the user downloading them
- +
Anything presentational that does not move
- −
useState,useEffect,useRefand every other hook - −
onClickand the other events - −
window,document,localStorage - −
Creating or reading context
„You're importing a component that needs useState. It only works in a Client Component, but none of its parents are marked with use client.“ Translated: you used a hook in a file running on the server. Either put "use client" at the top – or pull the interactive part into its own file.
Props across the boundary
// From server to client only things that can be sent:
<ClientPart
title="Hello" // ✅ text
count={3} // ✅ number
data={{ a: 1 }} // ✅ plain object
date={new Date()} // ✅ works too
onClick={() => …} // ❌ not functions
db={prismaClient} // ❌ not class instances
/> You may pass a function after all, if it is marked with "use server". Next.js then sends a reference rather than the function itself. Forms are built on exactly that – there is a lesson of its own on it later.
▸ In more depth: how does the result reach the browser? optional
The server does not send finished HTML for the server components but a compact description – the RSC payloadThe data format the server uses to send the result of its components to the browser – not HTML, but a description.→. It says: “text here, a hole there where client component Foo goes, with these props”.
1. Server runs the server components
2. The result is sent as an RSC payload
(+ finished HTML as well on the first request)
3. The browser downloads ONLY the client components' JavaScript
4. React stitches both together and brings the buttons to life That is why the code of your server components is never visible in the browser – not even in the developer tools. An API key there really is safe.
The server-onlyA tiny package that breaks the build if a file accidentally ends up in the browser. Protection for code holding secrets.→ package fails the build if a file holding secrets is ever imported by a client component. One import, one worry less.
Does it stick?
5 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
Here is the interactive part of a product card – the button that puts something in the cart. Finish it as a client component. (The editor runs plain React; in Next.js the file would additionally start with "use client".)
- □Before the click the button reads
Add to cart - □After the click it reads
In the cart (1) - □Another click makes it
In the cart (2) - □The count lives in an element with
data-testid="count"
import { useState } from "react"; // In Next.js this file would start with: "use client"; export function AddToCartButton() { // TODO: remember the count and label the button accordingly return <button data-testid="count">???</button>; } export default function App() { return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <h3>Espresso machine</h3> <AddToCartButton /> </div> ); }