Client Components
"use client" — The Key That Unlocks Interactivity
LinkedIn Hook
"I added
useStateto a component and my entire app crashed. No useful error message. Just a wall of red."Turns out, I was using a React hook inside a Server Component — and the fix was two words at the top of the file.
But here is what nobody tells you: those two words don't just affect the file you put them in. They change the rendering behavior of every component imported below it.
"use client"is the most misunderstood directive in Next.js. Developers either slap it on everything (destroying the performance benefits of Server Components) or avoid it entirely (building apps that can't respond to a button click).In Lesson 3.2, you'll learn exactly when "use client" is required, what triggers the need, how the client boundary works, and the cascade effect that trips up even senior developers in interviews.
Read the full lesson -> [link]
#NextJS #UseClient #ClientComponents #ReactServerComponents #WebDev #FrontendDevelopment #InterviewPrep
What You'll Learn
- What
"use client"actually does and why it exists in the App Router - The three triggers that force you to use it: interactivity, hooks, and browser APIs
- How the client boundary works — and why everything imported below it becomes client
- The cascade effect: how one
"use client"can silently bloat your JavaScript bundle - Patterns to minimize client boundaries while keeping your app interactive
- How to answer interview questions about Server vs Client Components with confidence
The Customs Checkpoint Analogy
Imagine a country with two zones: a Government Zone (server) and a Public Zone (client/browser).
In the Government Zone, you have access to classified databases, internal filing systems, and confidential records. You can read anything, process anything, and produce documents. But there is one rule: no one in the Government Zone can interact with citizens face-to-face. No handshakes, no conversations, no responding to taps on the shoulder.
The Public Zone is where citizens live. They can tap buttons, fill out forms, wave their hands, and react to things in real time. But they cannot walk into the Government Zone and read classified files.
Now, there is a Customs Checkpoint between the two zones. This checkpoint is "use client". The moment you declare it at the top of a file, you are saying: "Everything in this file — and everything this file imports — operates in the Public Zone."
Here is the critical part: once you cross that checkpoint into the Public Zone, you cannot go back. Every person (component) you bring with you through customs also becomes a Public Zone resident. You cannot import a Government Zone worker (Server Component) from inside the Public Zone.
That is the client boundary. It is a one-way gate. And understanding where to place it is the single most important architectural decision in a Next.js App Router application.
What Is "use client"?
"use client" is a directive — a special string literal placed at the very top of a JavaScript or TypeScript file. It tells the Next.js bundler: "This file, and everything it imports, should be included in the client-side JavaScript bundle."
// components/LikeButton.tsx
'use client'; // This MUST be the first line (before any imports)
import { useState } from 'react';
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked' : 'Like'}
</button>
);
}
// Without "use client", this file is a Server Component by default.
// useState would throw an error because hooks don't exist on the server.
// "use client" tells Next.js: "Ship this component's JavaScript to the browser."
Important clarification: "use client" does not mean the component only runs in the browser. Client Components are still pre-rendered on the server as HTML (for the initial page load), and then hydrated in the browser. The directive means: "This component needs JavaScript in the browser to function."
The Three Triggers — When You Must Use "use client"
There are exactly three categories of functionality that force you to add "use client". If your component does not need any of these, it should stay as a Server Component.
Trigger 1: Interactivity (Event Handlers)
Any component that responds to user actions needs client-side JavaScript.
// components/ToggleMenu.tsx
'use client'; // Required: onClick is an event handler
import { useState } from 'react';
export default function ToggleMenu() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
{/* onClick requires JavaScript in the browser */}
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close Menu' : 'Open Menu'}
</button>
{isOpen && (
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
)}
</div>
);
}
// Why? The server can render the initial HTML, but it cannot
// "listen" for a click. Event handlers only work in the browser
// where the DOM exists and user interactions happen.
Common event handlers that require "use client":
onClick,onChange,onSubmit,onFocus,onBluronMouseEnter,onMouseLeave,onKeyDown,onKeyUponScroll,onDrag,onDrop,onTouchStart
Trigger 2: React Hooks
Hooks manage state and side effects — both of which require a live browser environment.
// components/SearchInput.tsx
'use client'; // Required: useState + useEffect are hooks
import { useState, useEffect } from 'react';
export default function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// useEffect runs ONLY in the browser, after the component mounts
useEffect(() => {
if (query.length < 3) return;
const timeout = setTimeout(async () => {
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
setResults(data.items);
}, 300);
return () => clearTimeout(timeout);
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map((item: { id: string; title: string }) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
// Without "use client", this would crash:
// - useState doesn't exist on the server
// - useEffect doesn't exist on the server
// - onChange is an event handler (Trigger 1 also applies)
Hooks that require "use client":
useState— component-level stateuseEffect— side effects after renderuseRef— mutable references / DOM accessuseReducer— complex state logicuseContext— consuming React contextuseCallback,useMemo— memoization (these reference closures that live in the browser)useFormStatus,useOptimistic— form interactivity hooks- Any custom hook that internally uses any of the above
Trigger 3: Browser APIs
Anything that exists only in the browser (the window, document, navigator, localStorage, etc.) requires client-side execution.
// components/ThemeToggle.tsx
'use client'; // Required: localStorage and window are browser APIs
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
// localStorage only exists in the browser
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);
}, []);
const toggleTheme = () => {
const next = theme === 'light' ? 'dark' : 'light';
setTheme(next);
// window.document only exists in the browser
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
};
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
// The server has no localStorage, no document, no window.
// These are browser-only globals. Any component using them
// must be a Client Component.
Browser APIs that require "use client":
window(resize, scroll position, matchMedia)document(DOM manipulation, querySelector)localStorage/sessionStoragenavigator(geolocation, clipboard, user agent)IntersectionObserver,ResizeObserver,MutationObserverWeb Audio API,Canvas API,WebGLfetchinsideuseEffect(client-side data fetching)
The Client Boundary — The Most Important Concept
Here is where most developers get confused — and where interviewers test your understanding.
"use client" does not just affect the file it lives in. It creates a boundary. Every component imported into a "use client" file becomes part of the client bundle, even if that component does not have "use client" itself.
// components/UserCard.tsx
// NOTE: No "use client" here — this is a plain component
export default function UserCard({ name, email }: { name: string; email: string }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
}
// By itself, UserCard is a Server Component. It has no hooks,
// no event handlers, no browser APIs. Zero client JavaScript.
// components/InteractiveProfile.tsx
'use client'; // This creates the client boundary
import { useState } from 'react';
import UserCard from './UserCard'; // UserCard is now PULLED into the client bundle
export default function InteractiveProfile() {
const [showEmail, setShowEmail] = useState(false);
return (
<div>
{/* UserCard is rendered as a Client Component here */}
{/* Even though UserCard itself has no "use client" */}
<UserCard
name="Alice"
email={showEmail ? 'alice@example.com' : '***hidden***'}
/>
<button onClick={() => setShowEmail(!showEmail)}>
{showEmail ? 'Hide Email' : 'Show Email'}
</button>
</div>
);
}
// The boundary rule:
// InteractiveProfile has "use client" -> it's a Client Component
// UserCard is IMPORTED BY InteractiveProfile -> it's also a Client Component
// The import statement pulls UserCard across the boundary
Visualizing the Boundary
Component Tree (without careful boundaries):
app/page.tsx (Server Component)
|
+-- Header.tsx (Server Component) -- no JS sent to browser
|
+-- InteractiveProfile.tsx ("use client") -- CLIENT BOUNDARY
| |
| +-- UserCard.tsx -- pulled into client bundle (unnecessary!)
| |
| +-- Avatar.tsx -- pulled into client bundle (unnecessary!)
| |
| +-- Badge.tsx -- pulled into client bundle (unnecessary!)
|
+-- Footer.tsx (Server Component) -- no JS sent to browser
Everything under the "use client" boundary becomes client JavaScript.
UserCard, Avatar, and Badge might not need any interactivity,
but they are bundled as client code because they were IMPORTED
by a Client Component.
The Cascade Effect — Why One Directive Can Bloat Your Bundle
This is the architectural trap. Consider a real-world scenario:
// app/dashboard/page.tsx (Server Component)
import Sidebar from './Sidebar';
import MainContent from './MainContent';
import Analytics from './Analytics';
export default async function DashboardPage() {
// This data fetch happens on the server — zero client JS
const stats = await fetch('https://api.example.com/stats').then(r => r.json());
return (
<div className="dashboard">
<Sidebar />
<MainContent stats={stats} />
<Analytics />
</div>
);
}
Now imagine a junior developer needs to add a collapsible sidebar:
// app/dashboard/Sidebar.tsx — WRONG APPROACH
'use client'; // Added to make the sidebar collapsible
import { useState } from 'react';
import Navigation from './Navigation'; // 50 navigation items with icons
import UserProfile from './UserProfile'; // Avatar, name, role display
import TeamList from './TeamList'; // List of 20 team members
import NotificationBell from './NotificationBell'; // Notification counter
export default function Sidebar() {
const [collapsed, setCollapsed] = useState(false);
return (
<aside className={collapsed ? 'w-16' : 'w-64'}>
<button onClick={() => setCollapsed(!collapsed)}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
<UserProfile />
<Navigation />
<TeamList />
<NotificationBell />
</aside>
);
}
// PROBLEM: "use client" on Sidebar pulls EVERYTHING into the client bundle:
// - Navigation (50 items, icons, submenus) — could be server-rendered
// - UserProfile (static display) — could be server-rendered
// - TeamList (static list) — could be server-rendered
// - NotificationBell — this one actually needs client JS, fair enough
//
// Result: Potentially 100KB+ of unnecessary JavaScript sent to the browser
The Fix — Push the Boundary Down
The correct approach is to isolate the interactive part into the smallest possible Client Component:
// app/dashboard/Sidebar.tsx — CORRECT APPROACH (Server Component)
// No "use client" here — this stays on the server
import Navigation from './Navigation';
import UserProfile from './UserProfile';
import TeamList from './TeamList';
import NotificationBell from './NotificationBell';
import CollapsibleWrapper from './CollapsibleWrapper'; // Only THIS is client
export default function Sidebar() {
return (
<CollapsibleWrapper>
{/* These are passed as children — they remain Server Components */}
<UserProfile />
<Navigation />
<TeamList />
<NotificationBell />
</CollapsibleWrapper>
);
}
// app/dashboard/CollapsibleWrapper.tsx
'use client'; // Tiny Client Component — only the toggle logic
import { useState, type ReactNode } from 'react';
export default function CollapsibleWrapper({ children }: { children: ReactNode }) {
const [collapsed, setCollapsed] = useState(false);
return (
<aside className={collapsed ? 'w-16' : 'w-64'}>
<button onClick={() => setCollapsed(!collapsed)}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
{/* children are Server Components rendered on the server */}
{/* They pass through the Client Component as pre-rendered HTML */}
{!collapsed && children}
</aside>
);
}
// The children pattern:
// - CollapsibleWrapper is a Client Component (small, just toggle logic)
// - Its children (UserProfile, Navigation, etc.) are Server Components
// - Server Components are rendered on the server and passed as serialized HTML
// - The Client Component receives them as opaque React nodes
// - Result: Only the toggle button's JS is sent to the browser
This is the children pattern (also called the "donut pattern") — it is the most important composition technique in the App Router. The Client Component wraps the interactive shell, and the Server Components fill the content.
WRONG (fat boundary): CORRECT (thin boundary):
Sidebar ("use client") Sidebar (Server Component)
|-- Navigation (client!) |-- CollapsibleWrapper ("use client")
|-- UserProfile (client!) | |-- children (Server Components)
|-- TeamList (client!) |-- Navigation (server - 0 JS)
|-- NotificationBell (client!) |-- UserProfile (server - 0 JS)
|-- TeamList (server - 0 JS)
JS sent: ~120KB |-- NotificationBell (server - 0 JS)
JS sent: ~2KB
What Makes a Component a Client Component — The Complete Rule Set
A component becomes a Client Component in exactly two ways:
Rule 1: The file has "use client" at the top
'use client'; // This file is a Client Component
export default function MyComponent() {
// Client Component — regardless of what's inside
}
Rule 2: The component is imported by a Client Component
// helpers/FormatDate.tsx — no "use client" directive
export default function FormatDate({ date }: { date: string }) {
return <span>{new Date(date).toLocaleDateString()}</span>;
}
// If FormatDate is imported by a "use client" file, it becomes client.
// If FormatDate is imported by a server file, it stays server.
// The SAME component can be server or client depending on who imports it.
This is a subtle but critical point: a component's identity as server or client is not intrinsic — it depends on the import context.
What Does NOT Make a Component a Client Component
- Having props does not make it client
- Returning JSX does not make it client
- Using
async/awaitdoes not make it client (in fact, only Server Components can beasync) - Importing utility functions (pure math, string formatting) does not make it client
- Importing CSS modules or Tailwind classes does not make it client
"use client" Placement Rules
The directive has strict placement requirements:
// CORRECT — "use client" as the very first line
'use client';
import { useState } from 'react';
// ... rest of file
// CORRECT — comments before "use client" are allowed
// This is a client component for the dashboard
'use client';
import { useState } from 'react';
// ... rest of file
// WRONG — imports before "use client"
import { useState } from 'react';
'use client'; // TOO LATE — this will not work as expected
// WRONG — "use client" inside a function
export default function MyComponent() {
'use client'; // This does nothing here — must be at file top
}
Server Components Inside Client Components — The Rules
This is one of the most common interview questions. Here is the definitive answer:
You CANNOT import a Server Component into a Client Component directly.
// components/ClientWrapper.tsx
'use client';
// This will NOT work as expected
// ServerOnlyWidget will be treated as a Client Component
import ServerOnlyWidget from './ServerOnlyWidget';
export default function ClientWrapper() {
return (
<div>
<ServerOnlyWidget /> {/* This is now client-side, NOT server-side */}
</div>
);
}
You CAN pass Server Components as children or props to a Client Component.
// app/page.tsx (Server Component)
import ClientWrapper from '@/components/ClientWrapper';
import ServerOnlyWidget from '@/components/ServerOnlyWidget';
export default function Page() {
return (
// ServerOnlyWidget is rendered by the Server Component (page.tsx)
// and passed as a child to ClientWrapper
<ClientWrapper>
<ServerOnlyWidget /> {/* This stays a Server Component */}
</ClientWrapper>
);
}
// components/ClientWrapper.tsx
'use client';
import { type ReactNode } from 'react';
export default function ClientWrapper({ children }: { children: ReactNode }) {
// children is an opaque React node — already rendered on the server
// ClientWrapper doesn't import ServerOnlyWidget, so no boundary crossing
return <div className="interactive-shell">{children}</div>;
}
The key insight: when you pass a Server Component as children, the parent Server Component handles the rendering and serialization. The Client Component just receives the output — it never imports or executes the Server Component code.
Common Mistakes
-
Putting
"use client"on every component "just to be safe." This defeats the entire purpose of Server Components. Every"use client"file adds JavaScript to the client bundle. If a component has no interactivity, hooks, or browser APIs, leave it as a Server Component. Zero client JS is always better than unnecessary client JS. -
Adding
"use client"to a parent component when only a child needs it. If your page component needs one interactive button, do not make the entire page a Client Component. Extract the button into its own file with"use client"and import it. Push the boundary as far down the tree as possible. -
Trying to use
async/awaitin a Client Component. Client Components cannot beasyncfunctions. If you need to fetch data, either fetch it in a parent Server Component and pass it as props, or useuseEffectinside the Client Component for client-side fetching. -
Forgetting that the import creates the boundary, not the directive alone. Developers add
"use client"to one file and don't realize that every component imported into that file also becomes client. Always audit your import chains when adding the directive. -
Using
"use client"at the layout level. If yourlayout.tsxhas"use client", every page and component rendered inside that layout becomes client. This is almost never what you want. Keep layouts as Server Components and extract interactive elements (mobile nav toggles, theme switchers) into small Client Components.
Interview Questions
1. What does "use client" actually do in Next.js? Does it mean the component only runs in the browser?
No. "use client" marks a file as the entry point for the client bundle. The component is still pre-rendered on the server as HTML for the initial page load, then hydrated in the browser. The directive means: "This component needs JavaScript in the browser to function — include it in the client bundle." Without it, components are Server Components by default in the App Router, meaning zero JavaScript is sent to the browser for them.
2. Name the three categories of functionality that require "use client".
The three triggers are: (1) Interactivity — any event handlers like onClick, onChange, onSubmit; (2) React hooks — useState, useEffect, useRef, useContext, useReducer, and any custom hooks using them; (3) Browser APIs — window, document, localStorage, navigator, IntersectionObserver, and any Web API that only exists in the browser environment.
3. If Component A has "use client" and imports Component B (which has no directive), is Component B a Server Component or a Client Component?
Component B becomes a Client Component. The "use client" directive creates a boundary — every component imported into a Client Component file is pulled into the client bundle, regardless of whether that imported component has its own "use client" directive. The import is what crosses the boundary.
4. How can you render a Server Component inside a Client Component?
You cannot import a Server Component directly into a Client Component file. But you can pass a Server Component as children or as a prop from a parent Server Component. The parent Server Component renders the Server Component on the server, serializes the output, and passes it to the Client Component as an opaque React node. This is called the "children pattern" or "donut pattern." The Client Component receives pre-rendered content without ever importing or executing the Server Component code.
5. A senior developer on your team added "use client" to the main layout.tsx file because the mobile navigation needs a hamburger menu toggle. What would you recommend instead?
Adding "use client" to layout.tsx is a critical mistake — it turns every page and component nested inside that layout into a Client Component, massively bloating the JavaScript bundle. Instead, I would extract just the hamburger menu toggle into a small, dedicated Client Component (e.g., MobileNavToggle.tsx with "use client"), and keep layout.tsx as a Server Component. The layout can import MobileNavToggle and use it alongside other Server Components. This way, only the few bytes of toggle logic are sent as client JavaScript, while the rest of the layout (header, footer, navigation links, etc.) remain zero-JS Server Components. The principle is: push the client boundary as far down the component tree as possible.
Quick Reference -- Cheat Sheet
| Concept | Key Point |
|---|---|
"use client" is... | A directive marking a file as a client bundle entry point |
| Default in App Router | All components are Server Components unless marked otherwise |
| Three triggers | Interactivity (event handlers), Hooks (useState, useEffect), Browser APIs (window, localStorage) |
| Client boundary | Every component imported by a "use client" file becomes client |
| Cascade effect | One high-level "use client" can pull dozens of components into the client bundle |
| Children pattern | Pass Server Components as children to a Client Component to keep them server-rendered |
| Async components | Only Server Components can be async — Client Components cannot |
| Pre-rendering | Client Components are still server-rendered as HTML on initial load, then hydrated |
| Best practice | Push "use client" as far down the component tree as possible |
+-----------------------------------------------+
| "use client" Mental Model |
+-----------------------------------------------+
| |
| 1. Default = Server Component (0 JS) |
| 2. Need onClick/hooks/browser API? |
| -> Add "use client" to THAT file |
| 3. Boundary rule: |
| import inside "use client" = client |
| 4. Children passed from server = stay server |
| |
| Architecture principle: |
| "Push the boundary DOWN, not UP" |
| |
| +-- page.tsx (Server) ----------+ |
| | +-- DataDisplay (Server) | |
| | +-- StaticList (Server) | |
| | +-- InteractiveBtn (Client) | <-- tiny |
| | only 2KB of JS shipped | |
| +-------------------------------+ |
| |
| Decision rule: |
| "Does this component need to RESPOND |
| to user actions or access the browser?" |
| Yes -> "use client" |
| No -> Leave it as a Server Component |
| |
+-----------------------------------------------+
Previous: Lesson 3.1 -- React Server Components (RSC) Next: Lesson 3.3 -- Server vs Client Component Decision ->
This is Lesson 3.2 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.