Ch. 24 · Next.js

Next.js Server vs Client Components: When to Use Each

How Server and Client Components render in the Next.js 15 App Router, where to put use client, and how to keep secrets out of the bundle.

~7 min readintermediateupdated Oct 6, 2026

“How do you decide whether a component should be a Server Component or a Client Component?” sounds like a definitions question, but interviewers use it to probe design judgement. The follow-ups show what they are really after: “Can a Client Component render a Server Component?”, “Why did adding 'use client' to this page break the build?”, and “How do you make sure an API key never reaches the browser?”

A good answer explains the rendering pipeline, not just the rules. If you know what actually crosses from server to browser, the rules about hooks, props and imports stop being things to memorise and become consequences you can reason about.

Before you start

You need React basics: components, props, useState and useEffect. Know the difference between rendering HTML on the server and hydrating it in the browser. Familiarity with the App Router folder structure (app/page.tsx, app/layout.tsx) helps. Examples use Next.js 15 and React 19.

The short answer

In the App Router every component is a Server Component by default. Server Components run only on the server, can be async, can read databases and secrets, and send rendered output to the browser without their code. A file marked 'use client' starts a Client Component boundary: that module and everything it imports is bundled for the browser, pre-rendered to HTML on the server and then hydrated, so it can use state, effects and event handlers. Keep data fetching and static markup on the server, and push 'use client' down to the small interactive leaves.

How it works

Rendering a route happens in two passes on the server and one in the browser.

First, React renders the Server Components and produces the RSC payload: a serialized description of the tree. Server Components appear in it as their rendered output. Client Components appear as a reference to a JavaScript module plus the props passed to them. Second, Next.js uses that payload to produce HTML for the first page load, rendering the Client Components on the server too. In the browser, React reads the payload, loads the referenced client modules and hydrates only those parts.

// app/products/[id]/page.tsx: Server Component (no directive)
import { getProduct } from '@/lib/products';
import { FavouriteButton } from './FavouriteButton';

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);     // runs on the server only
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <FavouriteButton productId={product.id} />
    </article>
  );
}
TSX
// app/products/[id]/FavouriteButton.tsx: Client Component
'use client';
import { useState } from 'react';

export function FavouriteButton({ productId }: { productId: string }) {
  const [saved, setSaved] = useState(false);
  return <button onClick={() => setSaved(!saved)}>{saved ? 'Saved' : 'Save'}</button>;
}
TSX

The browser downloads code for FavouriteButton and nothing for ProductPage or getProduct. That explains the rules. A Server Component cannot use useState because nothing of it runs in the browser to hold the state. Props passed to a Client Component must be serializable because they travel inside the payload: strings, numbers, plain objects, arrays, dates and even promises work, but a normal function cannot be turned into data. Next.js 15 reports “Event handlers cannot be passed to Client Component props” when you try.

Step-by-step walkthrough

Step 1: Start on the server and find the interactive leaves

Write the page as a Server Component, then look for anything that needs the browser: state, effects, event handlers, window, localStorage, or a library that uses them. Each of those becomes a small Client Component. A search box with a controlled input is a leaf; the results list it filters may not need to be.

Step 2: Pass server content through children

A Client Component cannot import a Server Component, because the import would pull server code into the browser bundle. It can still render one that a server parent passes in as children or another prop.

// Collapsible.tsx
'use client';
import { useState } from 'react';

export function Collapsible({ title, children }: { title: string; children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <section>
      <button onClick={() => setOpen(!open)}>{title}</button>
      {open && children}
    </section>
  );
}
TSX
// page.tsx (server)
<Collapsible title="Reviews">
  <Reviews productId={id} />   {/* async Server Component, rendered on the server */}
</Collapsible>
TSX

Reviews is rendered on the server and arrives in the payload as finished output. Collapsible only decides whether to show it. This composition pattern is how you put providers, tabs and modals around server-rendered content.

Step 3: Design props as data

Pass ids and plain values, not whole objects you happen to have. Everything you pass is visible to anyone who opens the page source, because it is serialized into the payload. If a child needs to trigger a mutation, pass a Server Action, which is the one kind of function allowed to cross: it is sent as a reference that calls the server.

Step 4: Fence off server-only modules

Mark modules that touch secrets or the database with the server-only package.

// lib/products.ts
import 'server-only';
import { db } from './db';

export async function getProduct(id: string) {
  const row = await db.product.findUnique({ where: { id } });
  return { id: row.id, name: row.name, description: row.description }; // a DTO, not the raw row
}
TypeScript

If a Client Component ever imports this file, directly or through another module, next build fails with “You’re importing a component that needs “server-only”” instead of shipping it.

Worked scenario

A developer needs the favourite button to work and adds 'use client' to the top of page.tsx, so useState can live in the page. The build fails: the database driver imported through getProduct now has to be bundled for the browser, and the bundler reports errors such as Module not found: Can't resolve 'fs'. A colleague “fixes” it by moving the database call into a Route Handler and fetching it from a useEffect in the page.

The result works, but every visitor now downloads the whole page as JavaScript, sees a loading spinner instead of server-rendered content, and the product description no longer appears in the initial HTML that crawlers see. Worse, while debugging, someone passes the entire database row to the client to save time:

<FavouriteButton product={row} />   // row includes costPrice and supplierEmail
TSX

Those fields are now in the page source of every product page.

The fix is the structure shown above: remove 'use client' from the page, restore the server-side getProduct call, extract FavouriteButton into its own file with the directive, and pass only productId. Add import 'server-only' to the data module and return a DTO, so the mistake cannot happen silently again.

Common mistake

  • “Client Components only render in the browser.” They are pre-rendered on the server and hydrated. Code that touches window during render still breaks on the server.
  • Putting 'use client' on a layout or page “to be safe”. Every import below becomes client code and the benefit of Server Components disappears for that subtree.
  • Thinking 'use server' marks Server Components. It marks Server Functions (Server Actions). Server Components need no directive.
  • Importing a Server Component into a client file. It is treated as client code or fails. Pass it as children instead.
  • Reading React context in a Server Component. Context exists only in the client tree. Read the data directly on the server, or render a client consumer.
  • Assuming props are private. Anything passed to a Client Component is visible in the page.

Verify the behavior

Use the build output and the generated files to prove what ships.

npx next build
# First Load JS stays near the shared baseline for server-heavy pages:
# ├ ƒ /products/[id]       296 B         103 kB

# Put a unique string in the server data module and another in the client button.
# Only the client one may appear in browser assets.
grep -rl "SERVER_MARKER_7781" .next/static | wc -l   # 0
grep -rl "CLIENT_MARKER_ON" .next/static | wc -l     # 1
Terminal

Then add import 'server-only' to a data module and import it from a 'use client' file once, as an experiment. The build must fail. Remove the import again once you have seen the error.

Follow-up questions

Does a Client Component make all of its children client components? Only the ones it imports. Children passed in as props from a server parent stay Server Components.

How do you use a third-party component that uses hooks but has no directive? Re-export it from your own file that starts with 'use client', and import it from there.

Can a Server Component use context providers? It can render a provider that is a Client Component, and the provider’s consumers below can be client code. The Server Component itself cannot read the value.

When is a fully client-rendered page reasonable? Highly interactive tools, such as an editor or a drawing canvas, where nearly everything depends on browser state. Even then, the surrounding layout can stay on the server.

Interview exercise

A product page shows a list of reviews fetched from the database. Users can sort the list by date or rating with a dropdown, and the page should stay fast and indexable. Sketch which components are Server Components and which are Client Components.

Answer and reasoning

Keep the page and the review fetch on the server so the reviews are in the initial HTML and the database code never ships. The dropdown needs state and an event handler, so it is a Client Component. There are two good designs. If the review list is small, fetch it on the server and pass the reviews as plain data to a client SortableReviews component that sorts in memory, which gives instant sorting. If the list is large, make the dropdown update the URL (?sort=rating) with useRouter, and let the server page read searchParams and return sorted, paginated results, which keeps the client bundle tiny and the sorted view shareable. Either way, 'use client' sits on the leaf, not the page, and props are plain review objects without internal fields.

Continue learning

Practise more component-model questions in the Next.js interview questions and the Next.js MCQs. Read React Server Components and the client boundary for the React side, and Next.js App Router vs Pages Router for migration. The official reference is the Next.js 15 guide to Server and Client Components, with React’s use client directive docs.

More in Next.js

esc