Next.js Interview Prep
Rendering Strategies

Client-Side Rendering (CSR) in Next.js

Client-Side Rendering (CSR) in Next.js

LinkedIn Hook

You add "use client" to a component, and suddenly your page's Lighthouse SEO score drops 30 points.

You remove it, and your useState throws a server error.

Welcome to the most misunderstood directive in Next.js.

"use client" doesn't mean "render this on the client." It means "include this component's JavaScript in the client bundle." The HTML is still server-rendered. The component still hydrates. And if you get hydration wrong, React will silently destroy your server-rendered HTML and re-render from scratch — killing performance worse than a plain SPA.

In this lesson, I break down what CSR actually means in the App Router era — when you genuinely need it, how hydration works under the hood, why hydration mismatches happen, and the exact patterns to fix them.

If you've ever seen a flash of wrong content before your page "corrects itself" — you have a hydration bug, and this lesson will teach you why.

Read the full lesson → [link]

#NextJS #React #JavaScript #InterviewPrep #Frontend #CSR #Hydration #WebDev #100DaysOfCode


Client-Side Rendering (CSR) in Next.js thumbnail


What You'll Learn

  • What "use client" actually does (and what it does NOT do)
  • When CSR is genuinely needed in a Next.js App Router application
  • How hydration works — the process of making server HTML interactive
  • Why hydration mismatches happen and how to diagnose them
  • Concrete patterns to fix every common hydration mismatch scenario
  • Dynamic imports with ssr: false — the true client-only escape hatch

The Concept — What Is CSR in Next.js?

Analogy: The Furnished Apartment

Imagine you're moving into a new apartment. The landlord has two approaches:

SSR approach: The apartment is fully furnished when you arrive — furniture in place, lights on, kitchen stocked. You walk in and everything works. But the TV remote doesn't respond yet. A technician arrives, connects the remote to the TV, and now you can interact with everything. That technician is hydration.

Traditional CSR (plain React SPA): You arrive at an empty apartment. A truck shows up, unloads all the furniture, and assembles everything while you watch a loading spinner. Nothing works until the truck finishes.

Next.js "use client" components follow the SSR approach, not the SPA approach. The server still renders the HTML. The client JavaScript then "hydrates" that HTML — attaching event listeners and making it interactive. The user sees content immediately but has to wait a moment before buttons respond.

This is the critical misunderstanding: "use client" does not mean "skip server rendering." It means "include this component's JavaScript in the client bundle so it can hydrate and become interactive."


"use client" — What It Actually Does

The Directive

"use client";

import { useState } from "react";

// This component's JS is sent to the browser
// But its HTML is STILL server-rendered on first load
export default function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? "Liked" : "Like"}
    </button>
  );
}

What happens at each stage:

  1. Build/Request time: Next.js renders this component on the server, producing HTML: <button>Like</button>
  2. Browser receives: The HTML appears instantly — the user sees the "Like" button
  3. JavaScript loads: React's client bundle (including this component) downloads
  4. Hydration: React walks the server HTML, attaches the onClick handler, and initializes useState(false) — the button is now interactive

What "use client" does NOT do:

  • It does NOT skip server-side rendering of that component
  • It does NOT make the component render only in the browser
  • It does NOT mean the component is "bad" or "less optimized"
  • It does NOT affect components above it in the tree

The Client Boundary

"use client" creates a boundary. Every component imported inside a client component also becomes a client component — even if it doesn't have the directive.

"use client";

// SearchInput.tsx — this is a client component

import Suggestion from "./Suggestion"; // Suggestion is NOW also a client component
import { useState } from "react";

export default function SearchInput() {
  const [query, setQuery] = useState("");

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Suggestion query={query} />
    </div>
  );
}

This is why you should push "use client" as far down the component tree as possible — to keep the client boundary small and maximize the amount of code that stays server-only.


When CSR Is Genuinely Needed in Next.js

Not every component needs "use client". Here's the decision:

You NEED "use client" when:

1. Interactive widgets that use React hooks

"use client";

import { useState, useEffect } from "react";

// Accordion needs state to track open/close
export default function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {title} {isOpen ? "▼" : "▶"}
      </button>
      {isOpen && <div>{children}</div>}
    </div>
  );
}

2. User-specific data that changes per user (not cacheable)

"use client";

import { useState, useEffect } from "react";

// Shopping cart is unique per user — fetched client-side
export default function CartIcon() {
  const [itemCount, setItemCount] = useState(0);

  useEffect(() => {
    // Fetch user-specific cart data after hydration
    fetch("/api/cart")
      .then((res) => res.json())
      .then((data) => setItemCount(data.count));
  }, []);

  return <span>Cart ({itemCount})</span>;
}

3. Browser API access

"use client";

import { useEffect, useState } from "react";

// Geolocation only exists in the browser
export default function LocationDisplay() {
  const [coords, setCoords] = useState(null);

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((pos) => {
      setCoords({ lat: pos.coords.latitude, lng: pos.coords.longitude });
    });
  }, []);

  if (!coords) return <p>Detecting location...</p>;
  return <p>Lat: {coords.lat}, Lng: {coords.lng}</p>;
}

4. Event listeners (onClick, onChange, onSubmit, etc.)

Any component that handles user interaction events needs the client directive.

You do NOT need "use client" for:

  • Static content (text, images, non-interactive layouts)
  • Data fetching from databases or APIs (use server components with async/await)
  • SEO-critical content that doesn't need interactivity
  • Components that only receive and display props

Hydration Deep Dive

What Hydration Actually Is

Hydration is React's process of taking server-rendered HTML and making it interactive. Think of it as React "adopting" the existing DOM instead of creating it from scratch.

Step by step:

1. Server renders component tree → produces HTML string
2. Browser receives HTML → displays it immediately (fast First Contentful Paint)
3. Browser downloads React JS bundle
4. React calls hydrateRoot() instead of createRoot()
5. React walks the existing DOM, comparing it to what it would render
6. React attaches event listeners to existing DOM nodes
7. React initializes state (useState, useReducer, etc.)
8. Component is now fully interactive

The Hydration Contract

React makes a critical assumption during hydration: the HTML the server produced must match exactly what the client would render on the first pass. If they don't match, you get a hydration mismatch.

// React's internal logic (simplified):
// Server: render() → "<p>Hello</p>"
// Client: render() → "<p>Hello</p>"  ← MATCH: attach listeners, done
// Client: render() → "<p>Hi</p>"     ← MISMATCH: destroy server HTML, re-render

When a mismatch is detected, React falls back to client-side rendering — it throws away the server HTML and re-creates the DOM. This is the worst outcome: you pay the cost of SSR (server time) AND the cost of CSR (full client re-render), getting the benefits of neither.


Hydration Mismatch — Examples and Fixes

Mismatch 1: Using Date.now() or new Date()

"use client";

// BUG: Server renders at time X, client hydrates at time Y
export default function Timestamp() {
  return <p>Current time: {new Date().toLocaleTimeString()}</p>;
  // Server: "Current time: 10:30:00 AM"
  // Client: "Current time: 10:30:02 AM"  ← MISMATCH
}

Fix: Use useEffect to set time-dependent values after hydration

"use client";

import { useState, useEffect } from "react";

export default function Timestamp() {
  const [time, setTime] = useState("");

  useEffect(() => {
    // This runs only on the client, after hydration
    setTime(new Date().toLocaleTimeString());
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  // During hydration, both server and client render empty string — MATCH
  return <p>Current time: {time || "Loading..."}</p>;
}

Mismatch 2: Using window or localStorage

"use client";

// BUG: window doesn't exist on the server
export default function ThemeToggle() {
  const theme = window.localStorage.getItem("theme") || "light";
  // ERROR: window is not defined (server-side)
  return <button>{theme === "light" ? "Dark Mode" : "Light Mode"}</button>;
}

Fix: Access browser APIs inside useEffect

"use client";

import { useState, useEffect } from "react";

export default function ThemeToggle() {
  const [theme, setTheme] = useState("light"); // Safe default for SSR

  useEffect(() => {
    // Read from localStorage only on the client
    const saved = localStorage.getItem("theme");
    if (saved) setTheme(saved);
  }, []);

  const toggle = () => {
    const next = theme === "light" ? "dark" : "light";
    setTheme(next);
    localStorage.setItem("theme", next);
  };

  return <button onClick={toggle}>{theme === "light" ? "Dark Mode" : "Light Mode"}</button>;
}

Mismatch 3: Rendering Different Content Based on typeof window

"use client";

// BUG: Common anti-pattern that causes hydration mismatch
export default function Greeting() {
  const isBrowser = typeof window !== "undefined";
  return <p>{isBrowser ? "Client rendered" : "Server rendered"}</p>;
  // Server: "Server rendered"
  // Client: "Client rendered"  ← MISMATCH
}

Fix: Use useEffect + state for environment-dependent rendering

"use client";

import { useState, useEffect } from "react";

export default function Greeting() {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  // First render matches server; after mount, show client content
  return <p>{mounted ? "Fully interactive" : "Loading..."}</p>;
}

Mismatch 4: Invalid HTML Nesting

// BUG: <div> inside <p> is invalid HTML
// Browser auto-corrects the DOM, breaking React's expected tree
export default function Card() {
  return (
    <p>
      <div>This is inside a paragraph</div>  {/* Browser ejects this */}
    </p>
  );
}

Fix: Use valid HTML nesting

export default function Card() {
  return (
    <div>
      <div>This is properly nested</div>
    </div>
  );
}

Mismatch 5: Browser Extensions Modifying DOM

Browser extensions (Grammarly, password managers, translators) inject elements into the DOM. React sees nodes it didn't render and reports a mismatch. This is not your bug — but you can suppress it:

// suppressHydrationWarning tells React to skip the mismatch check
// for THIS specific element only (not its children)
<p suppressHydrationWarning>{someValue}</p>

Use suppressHydrationWarning sparingly — only when you know the mismatch is harmless and expected.


Dynamic Imports with ssr: false — True Client-Only Rendering

Sometimes you have a component that genuinely cannot run on the server at all — a chart library that accesses canvas, a map widget that requires window, or a WYSIWYG editor with deep browser dependencies.

For these, next/dynamic with ssr: false is the escape hatch:

import dynamic from "next/dynamic";

// This component will NOT be server-rendered at all
// It loads only in the browser, after JavaScript executes
const HeavyChart = dynamic(() => import("./HeavyChart"), {
  ssr: false,
  loading: () => <div>Loading chart...</div>,
});

// This is a Server Component — no "use client" needed here
export default function DashboardPage() {
  return (
    <div>
      <h1>Analytics Dashboard</h1>
      <p>Your data at a glance.</p>
      <HeavyChart />  {/* Rendered ONLY on the client */}
    </div>
  );
}

When to use dynamic with ssr: false:

  • Third-party libraries that crash on the server (Leaflet, Chart.js canvas mode, Quill editor)
  • Components that are meaningless without JavaScript (interactive playground, code editor)
  • Heavy components you want to lazy-load for performance

When NOT to use it:

  • As a blanket fix for hydration mismatches (fix the root cause instead)
  • For components that have SEO-relevant content (search engines won't see them)
  • As a replacement for proper "use client" + useEffect patterns

Client-Side Rendering (CSR) in Next.js visual 1


Common Mistakes

Mistake 1: Putting "use client" at the top of every component

This is the most common mistake. If a component doesn't use hooks, event handlers, or browser APIs, it should be a Server Component. Adding "use client" unnecessarily increases your JavaScript bundle size and reduces the benefits of the App Router.

Rule: Start with Server Components by default. Add "use client" only when the component genuinely requires interactivity.

Mistake 2: Thinking "use client" prevents server rendering

The HTML is still generated on the server for the initial page load. "use client" only means React will hydrate that component on the client. The component runs in both environments. That's exactly why hydration mismatches are possible — the code runs twice and must produce identical output.

Mistake 3: Using typeof window !== "undefined" in render logic

This check evaluates differently on server vs client, guaranteeing a hydration mismatch. Move browser-dependent logic into useEffect, which only runs on the client after hydration completes.

Mistake 4: Ignoring hydration warnings in the console

React logs hydration mismatches as warnings, not errors. Developers often dismiss them. But each mismatch forces React to discard server HTML and re-render — destroying your SSR performance benefit. Treat every hydration warning as a bug to fix.

Mistake 5: Using suppressHydrationWarning everywhere

This prop should be used for genuinely unavoidable mismatches (browser extension interference, third-party injection). Using it to silence warnings from fixable bugs hides real performance problems. Fix the mismatch instead.


Interview Questions

Q1: What does "use client" actually do in Next.js App Router?

It marks a client boundary. The component and everything it imports will have their JavaScript included in the client bundle. Crucially, the component is still server-rendered on the initial page load — the HTML is generated on the server. The directive tells Next.js that this component needs to be hydrated (made interactive) on the client side. Without the directive, the component would be a Server Component — rendered only on the server with zero JavaScript sent to the browser.

Q2: Explain hydration. What is it and why can it fail?

Hydration is the process where React takes server-rendered HTML and "attaches" interactivity to it — initializing state, registering event listeners, and connecting the component tree to React's reconciler. It fails when there's a mismatch between the HTML the server produced and what the client would render on its first pass. Common causes include using Date.now() (different timestamps), accessing window/localStorage during render (doesn't exist on server), and invalid HTML nesting (browsers auto-correct the DOM). When hydration fails, React discards the server HTML and re-renders from scratch — negating all SSR benefits.

Q3: When should you use next/dynamic with ssr: false versus "use client"?

Use "use client" when a component needs interactivity (hooks, events) but can still render meaningful HTML on the server. The server renders the initial HTML, and the client hydrates it. Use dynamic with ssr: false when a component literally cannot run on the server at all — for example, a library that directly accesses canvas, window, or the DOM in its initialization code. The ssr: false approach skips server rendering entirely, so the component's content won't be in the initial HTML and won't be indexed by search engines.

Q4: A user reports a "flash of wrong content" on page load. What's happening and how do you fix it?

This is a hydration mismatch. The server renders one version of the content, the browser displays it, then React hydrates and produces different content — causing a visible flash as the DOM is replaced. To diagnose: check the browser console for hydration warnings, look for server/client differences (timestamps, localStorage reads, typeof window checks, random values). To fix: move all browser-dependent and time-dependent logic into useEffect so the initial render matches on both server and client.

Q5: How does the client boundary work? If I add "use client" to component A, what happens to component B that A imports?

Component B automatically becomes a client component as well — its JavaScript is included in the client bundle and it will hydrate on the client. The "use client" directive creates a boundary: everything below it in the import tree is client code. This is why the best practice is to push "use client" as far down the tree as possible — a leaf component that needs useState should get the directive, not a parent layout. However, a client component can still receive Server Components as children props — those children remain server-rendered.


Quick Reference — Cheat Sheet

+------------------------------------+----------------------------------------------+
| Concept                            | Key Point                                    |
+------------------------------------+----------------------------------------------+
| "use client"                       | Includes JS in client bundle. Component      |
|                                    | is STILL server-rendered on first load.      |
+------------------------------------+----------------------------------------------+
| Server Component (default)         | Zero JS sent to browser. Cannot use hooks,   |
|                                    | events, or browser APIs.                     |
+------------------------------------+----------------------------------------------+
| Hydration                          | React attaches interactivity to existing     |
|                                    | server HTML. Must match client output.       |
+------------------------------------+----------------------------------------------+
| Hydration mismatch                 | Server HTML differs from client render.      |
|                                    | React destroys HTML and re-renders.          |
|                                    | Fix: move dynamic values to useEffect.       |
+------------------------------------+----------------------------------------------+
| dynamic(() => import(), {          | True client-only rendering. No server HTML.  |
|   ssr: false                       | Use for libraries that crash on server.      |
| })                                 |                                              |
+------------------------------------+----------------------------------------------+
| Client boundary                    | "use client" makes all imports below it      |
|                                    | client components too. Push it down.         |
+------------------------------------+----------------------------------------------+
| suppressHydrationWarning           | Silences mismatch for one element.           |
|                                    | Use sparingly — fix the real cause.          |
+------------------------------------+----------------------------------------------+
| useEffect for browser values       | localStorage, window, Date.now() — all       |
|                                    | must go in useEffect, not in render.         |
+------------------------------------+----------------------------------------------+

RULE: Default to Server Components. Add "use client" only when you need interactivity.
RULE: If server and client render differently → hydration mismatch → fix with useEffect.
RULE: "use client" ≠ client-only. It means "server-render + hydrate on client."

Previous: Lesson 2.3 — Incremental Static Regeneration (ISR) → Next: Lesson 2.5 — SSR vs SSG vs ISR vs CSR — The Decision Framework →


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

On this page