Module F-3·24 min read

The network boundary in plain terms, what "use client" actually costs, why Server Components are the default, and the five mistakes every engineer makes the first week.

F-3 — Server Components vs Client Components: First Contact

Who this is for: Developers who have the App Router file system model from F-2 but haven't internalised the Server Component mental model yet. This module is the biggest conceptual leap in the entire course. Take it slowly. The confusion most engineers feel with the App Router traces back to skimming or misunderstanding this module.


The Shift That Changes Everything

React has always been a client-side library. You import it, it runs in the browser, it manages the DOM. Every mental model you built around hooks, effects, event handlers, browser APIs — all of it was built around the assumption that your components run in the browser.

React Server Components break that assumption at the root.

Starting with Next.js 13 App Router and React 18, a component can now be an async function that runs on the server, fetches data directly from a database or API, renders HTML, and sends the result to the browser — without ever shipping a single byte of its code to the client.

This is not "SSR." Server-Side Rendering has existed since the early days of Next.js — the Pages Router did SSR via getServerSideProps. The difference is architectural:

  • Old SSR model: The entire page runs on the server (page-level function → props → component tree renders). The component code also ships to the browser and runs again during hydration.
  • RSC model: Individual components can be server-only. Their code never reaches the browser. They don't hydrate. They exist as rendered output only.

This distinction is why React Server Components are a genuine paradigm shift rather than a performance tweak.


Two Worlds, One Tree

In the App Router, your component tree is split across a boundary. On one side: Server Components. On the other: Client Components. Both render React, but they have different rules, different capabilities, and different costs.

Server Components:

  • Run on the server at request time (or build time for static pages)
  • Can be async — can await database queries, API calls, file reads
  • Cannot use hooks (useState, useEffect, useRef, etc.)
  • Cannot attach event handlers (onClick, onChange, etc.)
  • Cannot access browser APIs (window, document, localStorage)
  • Zero JavaScript sent to the browser — they don't hydrate

Client Components:

  • Run in the browser (and also on the server during SSR for the initial HTML)
  • Can use hooks — useState, useEffect, useRef, useContext, all of them
  • Can attach event handlers
  • Can access browser APIs
  • Their code ships to the JavaScript bundle — they hydrate in the browser

The key insight: Server Components are the default. When you create a file in the App Router and don't add any directive, it's a Server Component. You opt into the client side explicitly.


The 'use client' Directive

Adding 'use client' at the top of a file marks it as a Client Component. This creates a boundary — a point where the server-rendered tree hands off to the client-rendered tree.

tsx
tsx

The 'use client' directive does not mean "this only runs in the browser." It means "this is a Client Component." React will still server-render it for the initial HTML — your button will appear in the HTML document before JavaScript loads. But it will also hydrate in the browser, and its code will be in the JavaScript bundle.


The Boundary Rule: Client Components Cannot Import Server Components

This is the rule that catches every engineer off guard at least once:

A Client Component cannot import a Server Component.

If you do this:

tsx

The ServerComponent would be pulled into the client bundle — it would run in the browser just like any other imported module. The "server-only" contract is violated.

But you can pass Server Components as props to Client Components:

tsx
tsx

ServerSidebar is composed as a prop (or as children), not imported. It was already rendered on the server before being passed to ClientLayout. The Client Component receives it as opaque React node — it can render it, but it doesn't control it or ship its code.

This pattern — passing Server Components through Client Component slots — is how you keep expensive server logic isolated while still having interactive wrappers around it.


What Does 'use client' Actually Cost?

Every time you add 'use client', you're making a decision: the component and everything it imports will be in the JavaScript bundle shipped to the browser.

This isn't a reason to avoid Client Components — interactivity requires them. But it is a reason to think about where you place the boundary.

Consider a product page with a mostly static layout and one interactive element:

text

The boundary is pushed as deep as possible. Only the interactive section and its children are Client Components. The rest of the tree renders on the server, produces HTML, and ships zero JavaScript.

Compare this to the mistake pattern:

text

Both produce the same visible output. The first version ships ~4KB of JavaScript. The second ships the entire product page tree as JavaScript. For an e-commerce site with 100,000 product pages, that's the difference between instant load times and frustrated users.

The rule: push 'use client' as deep into the tree as possible. The boundary should be right at the first point where interactivity is genuinely needed.


The Five Mistakes

These are the errors every engineer makes in their first week with Server Components. Listed here so you can avoid them instead of discovering them via cryptic build errors.

Mistake 1: Adding 'use client' to every component "just in case"

Some engineers learn that Server Components can't use hooks, panic, and put 'use client' everywhere. This works but defeats the entire purpose of the App Router. You pay the bundle cost, you lose the ability to fetch data directly, and you've effectively built a Pages Router app with extra syntax.

The fix: only add 'use client' when the component genuinely needs hooks, event handlers, or browser APIs.

Mistake 2: Trying to use hooks in a Server Component

tsx

If you need state, the component needs to be a Client Component, or the state needs to live inside a child Client Component. Server Components are stateless by definition — they run once, produce output, and that's it.

Mistake 3: Passing non-serializable props across the boundary

Server Components can pass data to Client Components as props. But those props must be serializable — they have to travel across the network as JSON-like data.

tsx

Event handlers cannot be passed from Server Components to Client Components as props. If you need an event handler, either the component defining it must be a Client Component, or you use a Server Action (covered in P-2).

Mistake 4: Using context across the Server/Client boundary

React context (createContext, useContext) only works in Client Components. You cannot create context in a Server Component and consume it in a Client Component, or vice versa.

tsx

Data that needs to flow to Client Components either comes through props (for explicit, component-level passing) or through a Client Component context provider that wraps the relevant subtree.

Mistake 5: Importing server-only code in Client Components

tsx

This will likely fail at runtime in the browser (Prisma doesn't run in the browser), but it might fail silently in some configurations and produce a bloated bundle in others. The server-only package exists precisely for this:

ts

Adding import 'server-only' to any module that should never reach the browser turns a silent runtime failure into a loud build-time error. Use it for database clients, secret-reading utilities, and any file that handles credentials.


The Rendering Model in Practice

To make this concrete, here's what happens when a user navigates to /products/42 in a Next.js App Router application:

  1. Server receives the request. Next.js begins rendering app/products/[id]/page.tsx on the server.

  2. Server Components run. The page component (a Server Component) runs async functions, queries the database, and renders its tree — including any Server Component children.

  3. Client Component boundaries are encountered. When the server encounters a Client Component (anything marked 'use client'), it renders its initial HTML output on the server (this is the SSR pass). The component's JavaScript is scheduled to be sent to the browser.

  4. The response is streamed. HTML is streamed to the browser incrementally. The <Suspense> boundaries you've set up (either explicitly or via loading.tsx) determine the streaming shape — content above Suspense boundaries arrives first, content inside async Suspense boundaries streams in as it resolves.

  5. Browser receives HTML. The user sees content immediately — no blank screen, no JavaScript-boot wait.

  6. Hydration. The browser downloads the JavaScript for Client Components and "hydrates" them — attaching event handlers, initialising state, making them interactive. This happens in the background while the user is already reading content.

  7. Subsequent navigations. React Router takes over in the browser. Further navigations don't do a full page request — the client fetches only the RSC payload (a special JSON-like format) for the new page segment and re-renders the affected parts of the tree.

Server Components only run in steps 1–4. They don't participate in step 6 (hydration) at all.


The server-only and client-only Packages

Two small packages from the Next.js ecosystem that belong in every production codebase:

bash

import 'server-only' in any module that must never reach the browser. import 'client-only' in any module that must never run on the server (a module that accesses window, for example, where the server would throw a window is not defined error).

These packages contain nothing — they only throw at build time if imported in the wrong environment. They're guardrails, not logic. Put them in:

  • Database clients
  • Functions that read process.env secrets
  • Any utility that calls server-only Node.js APIs

And for client-only:

  • Browser analytics initialisation
  • Modules that reference window, document, or navigator
  • Any third-party SDK that explicitly requires a browser environment

Context and State Patterns for Server Components

Server Components being stateless doesn't mean your application is stateless. It means state lives where it belongs: in Client Components, in the URL (via searchParams), or in the server (via cookies or session).

The pattern you'll use constantly:

tsx

FilterBar is a Client Component that reads useSearchParams() and calls router.push() when the user changes filters. The URL changes, Next.js re-runs the Server Component with new searchParams, and the page re-renders with filtered data. No useState needed in the server component. No prop drilling of filter values. The URL is the state.

This URL-as-state pattern is idiomatic App Router code. Get comfortable with it early.


When to Choose Server vs Client

Use this as your decision tree:

Server Component when:

  • You're fetching data from a database, API, or filesystem
  • The component has no interactivity (no event handlers, no dynamic state)
  • You're rendering static or semi-static content
  • You're handling sensitive data (credentials, secrets) that must never reach the browser
  • Bundle size matters and the component has no browser-side reason to exist

Client Component when:

  • You need useState, useReducer, or useContext
  • You need useEffect, useRef, or other lifecycle hooks
  • You're attaching event handlers (onClick, onChange, onSubmit)
  • You need browser APIs (window, document, localStorage, navigator)
  • You're using a third-party library that doesn't support Server Components (most UI libraries, animation libraries, etc.)

When in doubt: start as a Server Component. If you hit a compiler error or runtime error related to hooks or browser APIs, that's the signal to add 'use client'. Don't pre-emptively add it.


Where We Go From Here

F-4 covers data fetching in the App Router — the practical mechanics of fetching data in Server Components, how fetch() behaves differently in Next.js, and how to use Suspense to stream multiple data sources independently.

The concepts from this module are the prerequisite for everything in the Practitioner phase. When you're in P-6 making decisions about the 4-tier caching architecture, you're fundamentally deciding which parts of your data fetching live in Server Components and how aggressively to cache them. When you're in P-2 writing Server Actions, you're writing server-side functions that can be called from Client Components — the exact boundary you've been learning about here.

That boundary is the skeleton of the entire App Router. Every pattern we cover assumes you've internalised it.


Knowledge Check

What happens if you try to pass an event handler (like an onClick function) as a prop from a Server Component to a Client Component?


Which of the following is a valid use case for the server-only package?


According to the decision tree for choosing between Server and Client components, when MUST you use a Client Component?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.