Next.js Interview Prep
Server and Client Components

Server vs Client Component Decision

Server vs Client Component Decision

LinkedIn Hook

"Should this component be a Server Component or a Client Component?"

That single question comes up in every Next.js interview. And most candidates get it wrong — not because they don't know the difference, but because they don't have a decision framework.

They guess. They say "use client everywhere to be safe." Or they fight the framework by trying to use useState inside a Server Component and wonder why it breaks.

In Lesson 3.3, I break down the exact decision table interviewers expect — plus the "children pattern" that lets you compose Server Components inside Client Components (yes, that's possible). This is the lesson that finally makes the component tree click.

Read the full lesson -> [link]

#NextJS #ReactServerComponents #WebDevelopment #FrontendDevelopment #InterviewPrep


Server vs Client Component Decision thumbnail


What You'll Learn

  • A decision table that instantly tells you whether a component should be Server or Client
  • How to compose Server Components inside Client Components using the children pattern (the "donut pattern")
  • The component tree mental model that makes Next.js architecture intuitive
  • Why "use client" is a boundary declaration, not a per-component toggle
  • The most common mistakes developers make when splitting Server and Client Components

The Sorting Hat Analogy

Think of Next.js as Hogwarts, and every component you write has to be sorted into one of two houses: Server House or Client House.

The Sorting Hat (your decision framework) looks at what the component needs to do and places it accordingly:

  • Does it need to fetch data from a database or API? Server House.
  • Does it need to respond to clicks, keystrokes, or hover events? Client House.
  • Does it need SEO-critical content rendered in the initial HTML? Server House.
  • Does it need browser APIs like window, localStorage, or navigator? Client House.
  • Does it just display static content with no interactivity? Server House (the default — and the cheaper option).

The critical rule: a component cannot belong to both houses simultaneously. It is either fully server or fully client. But here is where it gets interesting — components from different houses can live together in the same tree. A Server Component can render a Client Component as its child, and (with the right pattern) a Client Component can render Server Components passed to it as children.

+-----------------------------------------------------------+
|                THE SORTING HAT RULES                       |
+-----------------------------------------------------------+
|                                                            |
|  "Does it need interactivity?"                             |
|      YES --> Client House (purple #8b5cf6)                 |
|      NO  --> Keep checking...                              |
|                                                            |
|  "Does it need browser APIs?"                              |
|      YES --> Client House                                  |
|      NO  --> Keep checking...                              |
|                                                            |
|  "Does it need direct data access?"                        |
|      YES --> Server House (green #10b981)                  |
|      NO  --> Keep checking...                              |
|                                                            |
|  "None of the above?"                                      |
|      --> Server House (it's the default for good reason)   |
|                                                            |
+-----------------------------------------------------------+

The default is Server. You only opt into Client when you have a concrete reason. This is the opposite of how most React developers think — in traditional React, everything runs on the client. In Next.js App Router, everything runs on the server unless you explicitly say otherwise.


The Decision Table

This is the table you should have memorized for any interview. When someone asks "should this be a Server or Client Component?", run through these criteria:

+---------------------------+--------------------+--------------------+
|   Criteria                |  Server Component  |  Client Component  |
+---------------------------+--------------------+--------------------+
| Fetch data (DB, API,      |       YES          |        NO          |
| filesystem)               |                    | (use useEffect or  |
|                           |                    |  React Query)      |
+---------------------------+--------------------+--------------------+
| Access backend resources  |       YES          |        NO          |
| (env secrets, DB direct)  |                    |                    |
+---------------------------+--------------------+--------------------+
| Keep sensitive info on    |       YES          |        NO          |
| server (tokens, API keys) |                    | (exposed to user)  |
+---------------------------+--------------------+--------------------+
| Large dependencies        |       YES          |        NO          |
| (keep off client bundle)  | (zero JS shipped)  | (adds to bundle)   |
+---------------------------+--------------------+--------------------+
| SEO-critical content      |       YES          |    NOT IDEAL       |
|                           | (in initial HTML)  | (needs hydration)  |
+---------------------------+--------------------+--------------------+
| onClick, onChange,        |        NO          |       YES          |
| onSubmit event handlers   |                    |                    |
+---------------------------+--------------------+--------------------+
| useState, useReducer,     |        NO          |       YES          |
| useEffect                 |                    |                    |
+---------------------------+--------------------+--------------------+
| Browser APIs (window,     |        NO          |       YES          |
| localStorage, navigator)  |                    |                    |
+---------------------------+--------------------+--------------------+
| Custom hooks with state   |        NO          |       YES          |
| or effects                |                    |                    |
+---------------------------+--------------------+--------------------+
| Class components          |        NO          |       YES          |
+---------------------------+--------------------+--------------------+
| Default in App Router?    |       YES          |   NO (opt-in via   |
|                           |                    |   "use client")    |
+---------------------------+--------------------+--------------------+

The one-line rule: If a component needs to react to the user (pun intended), it is a Client Component. If it just needs to render content, keep it on the server.

Napkin AI Visual Prompt: "Dark gradient (#0a0e1a -> #111827). Two-column comparison table. Left column header 'Server Component' in emerald green (#10b981) with a server icon. Right column header 'Client Component' in purple (#8b5cf6) with a browser icon. Rows showing criteria with green checkmarks and red X marks. White monospace text. Title: 'The Decision Table'. Clean, minimal."


The Component Tree Mental Model

Here is the mental model that makes everything click. Think of your Next.js application as a tree of components. The root of the tree (your layout and page files) are Server Components by default. As you move down the tree, the moment you place a "use client" directive, you create a boundary. Everything below that boundary — every child, grandchild, and descendant of that Client Component — is also a Client Component, unless you use the children pattern.

              page.tsx (Server)
              /        \
         Header        Main (Server)
        (Server)       /     \
          |       ProductList  Sidebar (Server)
          |       (Server)        |
        NavBar       |         FilterPanel
       (Client)   ProductCard   ("use client")
       "use client"  (Server)      |
          |            |        FilterInput
       MenuToggle   AddToCart    (Client - inherited)
       (Client -    ("use client")
       inherited)      |
          |         CartCounter
       Dropdown     (Client - inherited)
       (Client -
       inherited)

Key observations from this tree:

  1. page.tsx, Header, Main, ProductList, Sidebar, and ProductCard are all Server Components. They ship zero JavaScript to the browser.
  2. NavBar has "use client" — it and its child MenuToggle and Dropdown are all Client Components.
  3. AddToCart has its own "use client" — it and its child CartCounter are Client Components.
  4. FilterPanel has "use client" — its child FilterInput is automatically a Client Component too.
  5. Notice how ProductCard is a Server Component even though its sibling AddToCart is not. The boundary is per-branch, not per-level.

The rule: "use client" is a fence. Everything on the other side of the fence (below it in the tree) runs on the client. But sibling branches are unaffected.

Napkin AI Visual Prompt: "Dark gradient (#0a0e1a -> #111827). Component tree diagram with nodes as rounded rectangles. Server components in emerald green (#10b981), client components in purple (#8b5cf6). Dashed horizontal line across the tree labeled 'use client boundary'. White connecting lines between nodes. White monospace labels. Title: 'The Component Tree Boundary Model'."


The Children Pattern (Donut Pattern)

Here is the question that trips up even experienced developers: Can you render a Server Component inside a Client Component?

The instinct says no — if "use client" makes everything below it a Client Component, how could a server component exist inside a client one?

The answer: you cannot import a Server Component into a Client Component, but you can pass it as children (or any other prop).

This is called the donut pattern — the Client Component is the donut (with its interactive shell), and the Server Component is the filling passed through the hole.

Why It Works

When you pass a Server Component as children to a Client Component, the Server Component is already rendered to HTML on the server before the Client Component receives it. The Client Component gets pre-rendered content, not a component it needs to execute. It is like receiving a finished painting in a frame — the frame (Client Component) doesn't need to know how the painting was made.

The Wrong Way (This Breaks)

// components/ClientWrapper.tsx
'use client';

// WRONG: Importing a Server Component inside a Client Component
// This forces ServerContent to become a Client Component
import ServerContent from './ServerContent';

export default function ClientWrapper() {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && <ServerContent />}
      {/* ServerContent is now a Client Component — it lost all server benefits */}
    </div>
  );
}

When you import ServerContent directly inside a "use client" file, Next.js treats it as a Client Component. It gets bundled into the client JavaScript. Any async/await, direct database calls, or server-only code inside ServerContent will break.

The Right Way (Children Pattern)

// components/ClientWrapper.tsx
'use client';

import { useState, ReactNode } from 'react';

// The Client Component accepts children — it does NOT import server components
export default function ClientWrapper({ children }: { children: ReactNode }) {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}
// app/page.tsx (Server Component)
import ClientWrapper from '@/components/ClientWrapper';
import ServerContent from '@/components/ServerContent';

// The composition happens HERE, in a Server Component
// ServerContent is rendered on the server, then passed as children
export default function Page() {
  return (
    <ClientWrapper>
      <ServerContent />
      {/* ServerContent stays a Server Component because it was composed */}
      {/* in a server context and passed as pre-rendered children */}
    </ClientWrapper>
  );
}
// components/ServerContent.tsx
// This stays a Server Component — no "use client" directive
// It can do all server things: async/await, direct DB access, etc.

export default async function ServerContent() {
  // This runs on the server only — never sent to the browser
  const data = await fetch('https://api.example.com/content', {
    headers: { Authorization: `Bearer ${process.env.SECRET_API_KEY}` },
  }).then(r => r.json());

  return (
    <div>
      <h2>{data.title}</h2>
      <p>{data.body}</p>
      {/* This HTML is pre-rendered on the server and passed to ClientWrapper */}
    </div>
  );
}

The Donut Pattern Visualized

+------------------------------------------------------------------+
|                  THE DONUT PATTERN                                 |
+------------------------------------------------------------------+
|                                                                   |
|   page.tsx (Server) composes everything:                          |
|                                                                   |
|   +------- ClientWrapper ("use client") -------+                  |
|   |                                             |                 |
|   |   [button onClick] [state management]       |                 |
|   |                                             |                 |
|   |   +---- children (hole in the donut) ---+   |                 |
|   |   |                                     |   |                 |
|   |   |   ServerContent (Server Component)  |   |                 |
|   |   |   - async data fetching             |   |                 |
|   |   |   - secret API keys                 |   |                 |
|   |   |   - zero JS shipped                 |   |                 |
|   |   |                                     |   |                 |
|   |   +-------------------------------------+   |                 |
|   |                                             |                 |
|   +---------------------------------------------+                |
|                                                                   |
|   The donut = Client (interactive shell)                          |
|   The filling = Server (pre-rendered content)                     |
|                                                                   |
+------------------------------------------------------------------+

Real-World Donut Pattern: Modal with Server Content

// components/Modal.tsx
'use client';

import { useState, ReactNode } from 'react';

// Client Component: handles the open/close UI interaction
export default function Modal({
  trigger,
  children,
}: {
  trigger: string;
  children: ReactNode;
}) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>{trigger}</button>
      {isOpen && (
        <div className="modal-overlay" onClick={() => setIsOpen(false)}>
          <div className="modal-content" onClick={(e) => e.stopPropagation()}>
            <button onClick={() => setIsOpen(false)}>Close</button>
            {children}
            {/* Server-rendered content appears here */}
          </div>
        </div>
      )}
    </>
  );
}
// app/products/[id]/page.tsx (Server Component)
import Modal from '@/components/Modal';
import ProductDetails from '@/components/ProductDetails';

export default async function ProductPage({ params }: { params: { id: string } }) {
  // Data fetching happens on the server
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(
    (r) => r.json()
  );

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      {/* Donut pattern: Modal (client) wraps ProductDetails (server) */}
      <Modal trigger="View Full Specs">
        <ProductDetails productId={params.id} />
        {/* ProductDetails is a Server Component — it fetches specs from DB */}
        {/* It's pre-rendered on the server and passed as children to Modal */}
      </Modal>
    </main>
  );
}

This pattern is incredibly powerful. The modal handles all the interactive behavior (open, close, click-outside-to-dismiss) on the client, while the content inside the modal is fetched and rendered on the server — with full access to the database, environment secrets, and zero JavaScript overhead.


Pushing "use client" Down the Tree

A principle that interviewers love to hear: push "use client" as far down the component tree as possible. The higher up you place the boundary, the more components become Client Components and the more JavaScript you ship to the browser.

// BAD: Making the entire page a Client Component
// app/blog/[slug]/page.tsx
'use client'; // Everything below is now client-side

import { useState } from 'react';

export default function BlogPost({ params }) {
  const [likes, setLikes] = useState(0);
  // Now the ENTIRE page is client-rendered
  // You lose: server-side data fetching, SEO benefits, smaller bundle
  return (
    <article>
      <h1>Blog Post Title</h1>
      <p>10 paragraphs of static content that did NOT need to be client-rendered...</p>
      <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>
    </article>
  );
}
// GOOD: Only the interactive part is a Client Component
// components/LikeButton.tsx
'use client';

import { useState } from 'react';

export default function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}
// app/blog/[slug]/page.tsx (stays a Server Component)
import LikeButton from '@/components/LikeButton';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  // Server-side data fetching — direct, fast, no client bundle cost
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(
    (r) => r.json()
  );

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      {/* Only this tiny component ships JavaScript to the browser */}
      <LikeButton />
    </article>
  );
}

The entire blog post is server-rendered (great for SEO, zero JS for the content), and only the 5-line LikeButton component ships JavaScript to the browser. This is the architecture interviewers want to see you reason about.


Common Mistakes

1. Adding "use client" at the top of every file. This defeats the purpose of Server Components entirely. You ship all your JavaScript to the browser, lose direct data access, expose environment variables, and eliminate the performance benefits of server rendering. "use client" should be an exception, not the rule.

2. Importing a Server Component directly inside a Client Component. This silently converts the Server Component into a Client Component. There is no error — it just stops working as a server component. Use the children/donut pattern instead.

3. Trying to use useState or useEffect in a Server Component. Server Components cannot have state or lifecycle effects because they run once on the server and produce static HTML. If you need interactivity, extract that piece into a separate Client Component and import it.

4. Putting "use client" on a page.tsx file. While technically valid, this makes the entire page a Client Component. Almost always, you should keep page.tsx as a Server Component and push client boundaries down to individual interactive components.

5. Passing functions as props from Server to Client Components. Functions are not serializable. You cannot pass a callback function from a Server Component to a Client Component as a prop. Use Server Actions instead, or define the function inside the Client Component.

6. Confusing the "use client" boundary with individual component behavior. "use client" does not mean "this component renders on the client only." Client Components are still pre-rendered (SSR) on the server for the initial HTML. The directive means "this component also hydrates on the client and can use browser APIs." The naming is misleading — think of it as "use client too."


Interview Questions

1. "How do you decide whether a component should be a Server Component or a Client Component?"

I follow a simple rule: Server Components are the default. I only add "use client" when a component needs interactivity (event handlers like onClick, onChange), React state (useState, useReducer), lifecycle effects (useEffect), or browser APIs (window, localStorage). Everything else stays as a Server Component — static UI, data fetching, layout, content display. The goal is to push "use client" as far down the component tree as possible so the minimum amount of JavaScript reaches the browser.

2. "Can a Client Component render a Server Component? How?"

Not by importing it directly — that would silently convert the Server Component into a Client Component. But you can achieve it through the children pattern (also called the donut pattern). The Server Component is composed as children in a parent Server Component file, then passed into the Client Component. The Server Component is pre-rendered on the server, and the Client Component receives the finished HTML through its children prop. This works because the Client Component does not need to execute the Server Component — it just renders pre-computed output.

3. "What happens if you use useState inside a Server Component?"

You get a build-time or runtime error. Server Components run once on the server to produce HTML — they have no concept of state, re-renders, or lifecycle. useState requires a component that persists in memory across renders and can trigger re-renders when state changes. That only happens on the client. The fix is to extract the stateful logic into a separate Client Component file with "use client" at the top.

4. "Explain the 'use client' boundary. If a component has 'use client', does that mean all its children are also client components?"

Yes — with a nuance. All components imported inside a "use client" file become Client Components, even if they don't have their own "use client" directive. The boundary cascades down through imports. However, components passed as children or other JSX props are not affected by the boundary. A Server Component passed as children to a Client Component remains a Server Component because it was composed in a server context, not imported inside the client file. This is why the children pattern is so important.

5. "You're building a dashboard with a sidebar navigation (with collapsible sections), a header with user info, and a main content area showing data from a database. How would you split this into Server and Client Components?"

The page layout (layout.tsx) and the main content area stay as Server Components — the content area fetches data directly from the database with no client JavaScript cost. The header component that displays the user name can be a Server Component (it reads the session on the server via cookies()), but if it has a dropdown menu on click, I would extract just the dropdown into a Client Component using the children pattern. The sidebar is a Server Component for the navigation links, but the collapsible toggle button is a small Client Component. The pattern is: the structure and data live on the server, the interactive "leaf" components at the edges use "use client".


Cheat Sheet

ScenarioComponent TypeWhy
Fetching data from DBServerDirect access, no API layer needed
Displaying markdown contentServerStatic output, zero JS
Form with validationClientNeeds useState, onChange
Navigation links (no toggle)ServerJust renders <a> tags
Hamburger menu toggleClientNeeds onClick, useState
Reading cookies/headersServercookies() and headers() are server-only
Using localStorageClientBrowser-only API
Rendering a chart libraryClientD3/Chart.js need DOM access
Image gallery (static)ServerJust renders <img> tags
Image carousel (swipeable)ClientNeeds touch events, state
Comment display listServerJust renders data
Comment input formClientNeeds onChange, onSubmit
Auth-gated layoutServerCheck session on server
Theme toggle buttonClientNeeds onClick, context

The golden rule: Default to Server. Opt into Client at the leaf level. Use the children pattern when you need server content inside an interactive wrapper.

+----------------------------------------------------------+
|         SERVER VS CLIENT SPECTRUM                         |
+----------------------------------------------------------+
|                                                           |
|  SERVER (default)                    CLIENT (opt-in)      |
|  Zero JS shipped                     JS bundle cost       |
|  Direct data access                  Browser APIs         |
|  SEO by default                      Interactivity        |
|  Secrets stay safe                   State management     |
|                                                           |
|  Layout -> Page -> Content      Button -> Form -> Modal   |
|                                                           |
|  Rule: keep "use client" at the LEAVES of the tree.       |
|                                                           |
+----------------------------------------------------------+

Prev: Lesson 3.2 -- Client Components and "use client" Next: Lesson 3.4 -- Server Actions


This is Lesson 3.3 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page