Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 15 min read

Server and client components

The one decision you keep making in Next.js

In one sentence

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.

Put another way

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

decision.txt
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)
app/products/page.tsx
// 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>
  );
}
app/products/AddToCartButton.tsx
"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>
  );
}
Only this small file reaches the browser. The product list and the database access stay on the server.

“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.

Where the boundary sits
the whole page moves to the browser
"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>
    </>
  );
}
only the button moves
// 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" */}
    </>
  );
}
Tip Passing server content into client components

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

Reach for it when …
  • +

    Fetch data, hit the database, read files, use secrets

  • +

    Use large dependencies without the user downloading them

  • +

    Anything presentational that does not move

Skip it when …
  • useState, useEffect, useRef and every other hook

  • onClick and the other events

  • window, document, localStorage

  • Creating or reading context

Pitfall The error message everybody sees once

„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

boundary.tsx
// 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
/>
Good to know The exception: server actions

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”.

sequence.txt
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.

Tip You can make that guarantee explicit

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

Drawing the boundary in the right place

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>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →