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
useStatethrows 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
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:
- Build/Request time: Next.js renders this component on the server, producing HTML:
<button>Like</button> - Browser receives: The HTML appears instantly — the user sees the "Like" button
- JavaScript loads: React's client bundle (including this component) downloads
- Hydration: React walks the server HTML, attaches the
onClickhandler, and initializesuseState(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"+useEffectpatterns
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), accessingwindow/localStorageduring 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. Usedynamicwithssr: falsewhen a component literally cannot run on the server at all — for example, a library that directly accessescanvas,window, or the DOM in its initialization code. Thessr: falseapproach 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 windowchecks, random values). To fix: move all browser-dependent and time-dependent logic intouseEffectso 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 needsuseStateshould get the directive, not a parent layout. However, a client component can still receive Server Components aschildrenprops — 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.