Module 12 — Client Components in Next.js
Client Components are one of the most important concepts in the Next.js App Router. They allow you to build interactive interfaces that need browser capabilities such as state, effects, event handlers, and client-side APIs.
Understanding when to use a Client Component—and when not to—is essential for building fast, secure, and maintainable Next.js applications.
1. What Is a Client Component?
In the Next.js App Router, components are Server Components by default.
A Client Component is a React component that is explicitly marked with the "use client" directive.
1"use client"; 2 3export default function Counter() { 4 return <button>Click me</button>; 5}
The "use client" directive tells Next.js that this component belongs to the client-side React tree and may use browser-only features and interactive React APIs.
1Server Component 2 │ 3 │ renders 4 ▼ 5HTML + RSC Payload 6 │ 7 ▼ 8Browser 9 │ 10 ▼ 11Client Component 12 │ 13 ├── State 14 ├── Effects 15 ├── Events 16 └── Browser APIs
The important point is that "use client" does not mean that the entire application becomes client-side.
It creates a boundary between Server Components and Client Components.
2. Why Do We Need Client Components?
Server Components are excellent for:
- fetching data
- accessing databases
- keeping secrets on the server
- reducing browser JavaScript
- rendering static content
- improving initial performance
But Server Components cannot directly provide browser-side interactivity such as:
1useState 2useEffect 3onClick 4onChange 5localStorage 6window 7document 8navigator
For these features, you need a Client Component.
For example, this will not work as a Server Component:
1export default function Counter() { 2 const [count, setCount] = useState(0); 3 4 return ( 5 <button onClick={() => setCount(count + 1)}> 6 {count} 7 </button> 8 ); 9}
You need:
1"use client"; 2 3import { useState } from "react"; 4 5export default function Counter() { 6 const [count, setCount] = useState(0); 7 8 return ( 9 <button onClick={() => setCount(count + 1)}> 10 Count: {count} 11 </button> 12 ); 13}
Now React can maintain state inside the browser.
3. The "use client" Directive
The directive must appear before imports.
1"use client"; 2 3import { useState } from "react";
Incorrect:
1import { useState } from "react"; 2 3"use client";
The directive should be at the top of the module.
A common misconception is that "use client" is a JavaScript keyword.
It is actually a directive understood by Next.js and the React tooling.
4. Client Components and the Component Tree
Consider this structure:
1app/ 2└── dashboard/ 3 ├── page.tsx 4 └── SearchBox.tsx
The page can remain a Server Component:
1import SearchBox from "./SearchBox"; 2 3export default async function DashboardPage() { 4 const users = await getUsers(); 5 6 return ( 7 <main> 8 <h1>Dashboard</h1> 9 10 <SearchBox /> 11 12 <ul> 13 {users.map((user) => ( 14 <li key={user.id}>{user.name}</li> 15 ))} 16 </ul> 17 </main> 18 ); 19}
The interactive search box can be a Client Component:
1"use client"; 2 3import { useState } from "react"; 4 5export default function SearchBox() { 6 const [query, setQuery] = useState(""); 7 8 return ( 9 <input 10 value={query} 11 onChange={(event) => setQuery(event.target.value)} 12 placeholder="Search users..." 13 /> 14 ); 15}
This is a powerful architecture:
1DashboardPage 2 │ 3 ├── Server Component 4 │ └── Fetch database data 5 │ 6 └── SearchBox 7 │ 8 └── Client Component 9 └── useState()
You don't need to convert the entire dashboard into a Client Component just because one part is interactive.
5. Use Client Components for useState
One of the most common reasons to create a Client Component is useState.
1"use client"; 2 3import { useState } from "react"; 4 5export default function Counter() { 6 const [count, setCount] = useState(0); 7 8 return ( 9 <div> 10 <p>Count: {count}</p> 11 12 <button onClick={() => setCount(count + 1)}> 13 Increase 14 </button> 15 </div> 16 ); 17}
Here:
1const [count, setCount] = useState(0);
creates local browser-side state.
When the user clicks the button:
1setCount(count + 1);
React updates the component.
6. Use Client Components for useEffect
useEffect is another common reason for using a Client Component.
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function UserProfile() { 6 const [online, setOnline] = useState(false); 7 8 useEffect(() => { 9 setOnline(navigator.onLine); 10 }, []); 11 12 return ( 13 <p> 14 Status: {online ? "Online" : "Offline"} 15 </p> 16 ); 17}
The important part is:
1navigator.onLine
navigator is a browser API.
It does not exist in the normal server runtime.
Therefore this logic belongs in a Client Component.
7. Browser APIs
Client Components can interact with browser APIs such as:
1window 2document 3navigator 4localStorage 5sessionStorage 6IntersectionObserver 7ResizeObserver 8Clipboard API 9Geolocation API
For example:
1"use client"; 2 3export default function BrowserInfo() { 4 const width = window.innerWidth; 5 6 return ( 7 <p> 8 Browser width: {width}px 9 </p> 10 ); 11}
However, accessing browser APIs during rendering can cause problems when the component is initially rendered.
A safer pattern for browser-dependent values is often:
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function BrowserWidth() { 6 const [width, setWidth] = useState<number | null>(null); 7 8 useEffect(() => { 9 setWidth(window.innerWidth); 10 }, []); 11 12 return ( 13 <p> 14 Width: {width ?? "Loading..."}px 15 </p> 16 ); 17}
8. Browser Storage
Client Components are useful when working with:
1localStorage
or:
1sessionStorage
Example:
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function ThemePreference() { 6 const [theme, setTheme] = useState("light"); 7 8 useEffect(() => { 9 const savedTheme = localStorage.getItem("theme"); 10 11 if (savedTheme) { 12 setTheme(savedTheme); 13 } 14 }, []); 15 16 function changeTheme(value: string) { 17 setTheme(value); 18 localStorage.setItem("theme", value); 19 } 20 21 return ( 22 <div> 23 <p>Theme: {theme}</p> 24 25 <button onClick={() => changeTheme("dark")}> 26 Dark Mode 27 </button> 28 </div> 29 ); 30}
localStorage exists in the browser, so this functionality belongs on the client.
9. Event Handlers
Event handlers are another major reason to use Client Components.
For example:
1"use client"; 2 3export default function Button() { 4 function handleClick() { 5 alert("Button clicked!"); 6 } 7 8 return ( 9 <button onClick={handleClick}> 10 Click Me 11 </button> 12 ); 13}
Common event handlers include:
1onClick 2onChange 3onSubmit 4onFocus 5onBlur 6onMouseEnter 7onKeyDown
Interactive forms therefore commonly contain Client Components.
10. Interactive Forms
Consider a search form:
1"use client"; 2 3import { FormEvent, useState } from "react"; 4 5export default function SearchForm() { 6 const [query, setQuery] = useState(""); 7 8 function handleSubmit(event: FormEvent<HTMLFormElement>) { 9 event.preventDefault(); 10 11 console.log("Searching for:", query); 12 } 13 14 return ( 15 <form onSubmit={handleSubmit}> 16 <input 17 value={query} 18 onChange={(event) => setQuery(event.target.value)} 19 placeholder="Search..." 20 /> 21 22 <button type="submit"> 23 Search 24 </button> 25 </form> 26 ); 27}
The component needs:
useStateonChangeonSubmit
Therefore it is a good candidate for a Client Component.
11. Client-Side Libraries
Some React libraries require browser functionality.
Examples include libraries for:
- charts
- animations
- drag and drop
- maps
- rich text editors
- browser-based media
- interactive visualizations
For example:
1"use client"; 2 3import { useState } from "react"; 4 5export default function InteractiveChart() { 6 const [range, setRange] = useState("7d"); 7 8 return ( 9 <div> 10 <button onClick={() => setRange("7d")}> 11 7 Days 12 </button> 13 14 <button onClick={() => setRange("30d")}> 15 30 Days 16 </button> 17 18 <p>Showing data for: {range}</p> 19 </div> 20 ); 21}
If the library depends on browser APIs or React client features, isolate it inside a Client Component rather than turning an entire page into a Client Component.
12. The Most Important Rule: Keep Client Components Small
A common beginner mistake is putting "use client" at the top of a large page.
For example:
1"use client"; 2 3export default function ProductPage() { 4 // huge page 5}
This may unnecessarily move a large part of the component tree into the client boundary.
Instead, separate interactive functionality.
1ProductPage 2│ 3├── ProductDetails 4│ └── Server Component 5│ 6├── ProductDescription 7│ └── Server Component 8│ 9├── ProductReviews 10│ └── Server Component 11│ 12└── AddToCartButton 13 └── Client Component
Then:
1import AddToCartButton from "./AddToCartButton"; 2 3export default async function ProductPage() { 4 const product = await getProduct(); 5 6 return ( 7 <main> 8 <h1>{product.name}</h1> 9 10 <p>{product.description}</p> 11 12 <AddToCartButton productId={product.id} /> 13 </main> 14 ); 15}
The interactive button is isolated:
1"use client"; 2 3export default function AddToCartButton({ 4 productId, 5}: { 6 productId: string; 7}) { 8 function handleAdd() { 9 console.log("Adding product:", productId); 10 } 11 12 return ( 13 <button onClick={handleAdd}> 14 Add to Cart 15 </button> 16 ); 17}
This architecture keeps the client boundary small.
13. Server Components Can Render Client Components
A Server Component can import and render a Client Component.
1import LikeButton from "./LikeButton"; 2 3export default async function PostPage() { 4 const post = await getPost(); 5 6 return ( 7 <article> 8 <h1>{post.title}</h1> 9 10 <p>{post.content}</p> 11 12 <LikeButton postId={post.id} /> 13 </article> 14 ); 15}
The Client Component:
1"use client"; 2 3import { useState } from "react"; 4 5export default function LikeButton({ 6 postId, 7}: { 8 postId: string; 9}) { 10 const [liked, setLiked] = useState(false); 11 12 return ( 13 <button onClick={() => setLiked(!liked)}> 14 {liked ? "Liked ❤️" : "Like 🤍"} 15 </button> 16 ); 17}
This is one of the most useful patterns in the Next.js App Router.
1Server Component 2 │ 3 ├── Server content 4 │ 5 └── Client Component 6 │ 7 ├── State 8 ├── Events 9 └── Browser interaction
14. Passing Data from Server to Client
Server Components can pass serializable data to Client Components through props.
Server Component:
1import UserCard from "./UserCard"; 2 3export default async function Page() { 4 const user = await getUser(); 5 6 return <UserCard user={user} />; 7}
Client Component:
1"use client"; 2 3type User = { 4 id: string; 5 name: string; 6}; 7 8export default function UserCard({ 9 user, 10}: { 11 user: User; 12}) { 13 return ( 14 <div> 15 <h2>{user.name}</h2> 16 <p>User ID: {user.id}</p> 17 </div> 18 ); 19}
The server can fetch the data, while the client handles interaction.
This creates a clean separation:
1Server 2│ 3├── Database 4├── Authentication 5├── Data fetching 6└── Business logic 7 │ 8 │ props 9 ▼ 10Client 11│ 12├── State 13├── Events 14├── Browser APIs 15└── Interactive UI
15. Do Not Pass Server-Only Secrets to Client Components
This is extremely important.
Never expose secrets through Client Component props.
Bad:
1const secret = process.env.DATABASE_PASSWORD; 2 3return <ClientComponent secret={secret} />;
A Client Component is part of the browser-side application boundary.
Database passwords, private API keys, tokens, and other secrets must remain on the server.
A safer architecture is:
1Client Component 2 │ 3 │ request 4 ▼ 5Server 6 │ 7 ├── Secret API key 8 ├── Database 9 └── Private operations
For example:
1"use client"; 2 3export default function GenerateButton() { 4 async function generate() { 5 const response = await fetch("/api/generate"); 6 7 const data = await response.json(); 8 9 console.log(data); 10 } 11 12 return ( 13 <button onClick={generate}> 14 Generate 15 </button> 16 ); 17}
The server route can safely access private resources.
1export async function POST() { 2 const secret = process.env.PRIVATE_API_KEY; 3 4 // Use the secret on the server. 5 6 return Response.json({ 7 success: true, 8 }); 9}
The browser never needs to receive the secret itself.
16. Client Components Cannot Directly Access the Database
Avoid this architecture:
1"use client"; 2 3import { db } from "@/lib/db"; 4 5export default function Users() { 6 // ❌ Don't access the database directly here. 7}
Instead:
1Client Component 2 │ 3 │ request 4 ▼ 5Server Action / Route Handler / Server Component 6 │ 7 ▼ 8Database
For example:
1"use client"; 2 3export default function DeleteButton({ 4 userId, 5}: { 6 userId: string; 7}) { 8 async function deleteUser() { 9 await fetch(`/api/users/${userId}`, { 10 method: "DELETE", 11 }); 12 } 13 14 return ( 15 <button onClick={deleteUser}> 16 Delete 17 </button> 18 ); 19}
The server performs the database operation.
17. Client Component Boundaries
Once a file contains:
1"use client";
it establishes a Client Component boundary.
Think of it like a border:
1SERVER WORLD 2──────────────────────── 3Database 4Secrets 5Server APIs 6Server Components 7Async data fetching 8──────────────────────── 9 ↓ 10CLIENT BOUNDARY 11 ↓ 12──────────────────────── 13CLIENT WORLD 14State 15Effects 16Events 17Browser APIs 18Interactive UI 19────────────────────────
Understanding this boundary is more important than simply memorizing "use client".
18. A Real-World Search Example
Imagine a course website.
The page itself can remain a Server Component:
1import CourseList from "./CourseList"; 2 3export default async function CoursesPage() { 4 const courses = await getCourses(); 5 6 return ( 7 <main> 8 <h1>Courses</h1> 9 10 <CourseList courses={courses} /> 11 </main> 12 ); 13}
The filtering UI can be a Client Component:
1"use client"; 2 3import { useMemo, useState } from "react"; 4 5type Course = { 6 id: string; 7 title: string; 8}; 9 10export default function CourseList({ 11 courses, 12}: { 13 courses: Course[]; 14}) { 15 const [query, setQuery] = useState(""); 16 17 const filteredCourses = useMemo(() => { 18 return courses.filter((course) => 19 course.title 20 .toLowerCase() 21 .includes(query.toLowerCase()) 22 ); 23 }, [courses, query]); 24 25 return ( 26 <section> 27 <input 28 value={query} 29 onChange={(event) => setQuery(event.target.value)} 30 placeholder="Search courses..." 31 /> 32 33 <div> 34 {filteredCourses.map((course) => ( 35 <article key={course.id}> 36 <h2>{course.title}</h2> 37 </article> 38 ))} 39 </div> 40 </section> 41 ); 42}
The architecture becomes:
1CoursesPage 2 │ 3 ├── Server 4 │ └── Fetch courses 5 │ 6 └── CourseList 7 │ 8 └── Client 9 ├── useState 10 ├── useMemo 11 └── onChange
This is usually much better than making the entire page client-side.
19. Server vs Client Components
| Feature | Server Component | Client Component |
|---|---|---|
| Database access | ✅ | ❌ |
| Private environment variables | ✅ | ❌ |
| Server-side data fetching | ✅ | ❌ |
useState | ❌ | ✅ |
useEffect | ❌ | ✅ |
onClick | ❌ | ✅ |
| Browser APIs | ❌ | ✅ |
localStorage | ❌ | ✅ |
| Interactive UI | Limited | ✅ |
| Browser event handling | ❌ | ✅ |
| Client-side libraries | Limited | ✅ |
The goal is not to choose one exclusively.
Modern Next.js applications normally use both.
20. Common Mistake: Making Everything Client-Side
Beginners often write:
1"use client";
at the top of every file.
That defeats one of the major benefits of the App Router.
Instead, ask:
Does this component actually need browser-side functionality?
If the answer is no, keep it as a Server Component.
For example:
1export default function Article() { 2 return ( 3 <article> 4 <h1>Next.js Server Components</h1> 5 <p> 6 This article doesn't need client-side state. 7 </p> 8 </article> 9 ); 10}
There is no reason to add:
1"use client";
21. Common Mistake: Using window in a Server Component
This is incorrect:
1export default function Page() { 2 const width = window.innerWidth; 3 4 return <p>{width}</p>; 5}
window is a browser API.
Use a Client Component:
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function Page() { 6 const [width, setWidth] = useState<number | null>(null); 7 8 useEffect(() => { 9 setWidth(window.innerWidth); 10 }, []); 11 12 return <p>{width ?? "Loading..."}</p>; 13}
22. Common Mistake: Putting Data Fetching Everywhere in Client Components
You might see:
1"use client"; 2 3import { useEffect, useState } from "react"; 4 5export default function Products() { 6 const [products, setProducts] = useState([]); 7 8 useEffect(() => { 9 fetch("/api/products") 10 .then((response) => response.json()) 11 .then(setProducts); 12 }, []); 13 14 return <div>...</div>; 15}
This can work, but it isn't automatically the best architecture.
If the data can be fetched on the server, consider:
1export default async function Products() { 2 const products = await getProducts(); 3 4 return <ProductList products={products} />; 5}
Then use a Client Component only where interaction is actually required.
23. The Hybrid Architecture
The most powerful pattern is often:
1 Next.js Page 2 │ 3 ┌──────────┴──────────┐ 4 │ │ 5 SERVER SERVER 6 Product Data User Data 7 │ │ 8 └──────────┬──────────┘ 9 │ 10 ▼ 11 Client Component 12 │ 13 ┌──────────┼──────────┐ 14 │ │ │ 15 State Events Effects
This gives you the benefits of both architectures.
Server side
Use for:
1Data fetching 2Database access 3Authentication 4Secrets 5SEO content 6Server-side rendering
Client side
Use for:
1State 2Events 3Effects 4Browser APIs 5Animations 6Interactive controls 7Client-side libraries
24. A Practical Decision Rule
Before adding "use client", ask these questions:
1Does this component use useState? 2 │ 3 YES ──→ Client Component 4 5Does it use useEffect? 6 │ 7 YES ──→ Client Component 8 9Does it use browser APIs? 10 │ 11 YES ──→ Client Component 12 13Does it need event handlers? 14 │ 15 YES ──→ Client Component 16 17Does it require an interactive client library? 18 │ 19 YES ──→ Client Component 20 21Otherwise 22 │ 23 └──→ Prefer Server Component
This simple decision process prevents unnecessary client-side code.
25. Best Practices
1. Keep Server Components as the default
Don't add "use client" unless you need it.
2. Keep Client Components small
Create focused interactive components.
3. Fetch server data on the server when practical
Don't automatically move all fetching into useEffect.
4. Keep secrets on the server
Never expose private keys or database credentials to browser code.
5. Pass data through props
Use Server Components to prepare data and Client Components to interact with it.
6. Separate UI responsibilities
For example:
1ProductPage 2├── ProductInfo → Server 3├── ProductReviews → Server 4├── AddToCartButton → Client 5├── QuantitySelector → Client 6└── ShareButton → Client
7. Think in boundaries
The important question isn't:
"Is this page a Client Component?"
Instead ask:
"Which smallest part of this UI actually needs to run interactively in the browser?"
26. Complete Example
Here is a realistic product page.
Server Component
1import AddToCartButton from "./AddToCartButton"; 2 3export default async function ProductPage() { 4 const product = await getProduct(); 5 6 return ( 7 <main className="space-y-6"> 8 <h1 className="text-3xl font-bold"> 9 {product.name} 10 </h1> 11 12 <p>{product.description}</p> 13 14 <p className="text-xl"> 15 ${product.price} 16 </p> 17 18 <AddToCartButton productId={product.id} /> 19 </main> 20 ); 21}
Client Component
1"use client"; 2 3import { useState } from "react"; 4 5export default function AddToCartButton({ 6 productId, 7}: { 8 productId: string; 9}) { 10 const [loading, setLoading] = useState(false); 11 12 async function addToCart() { 13 setLoading(true); 14 15 try { 16 await fetch("/api/cart", { 17 method: "POST", 18 headers: { 19 "Content-Type": "application/json", 20 }, 21 body: JSON.stringify({ 22 productId, 23 }), 24 }); 25 } finally { 26 setLoading(false); 27 } 28 } 29 30 return ( 31 <button 32 onClick={addToCart} 33 disabled={loading} 34 className="rounded-lg px-4 py-2" 35 > 36 {loading ? "Adding..." : "Add to Cart"} 37 </button> 38 ); 39}
Notice the separation:
1ProductPage 2│ 3├── Fetch product 4│ ↓ 5│ SERVER 6│ 7└── AddToCartButton 8 ↓ 9 CLIENT 10 │ 11 ├── useState 12 ├── onClick 13 └── browser request
This is the architecture you should aim for in production Next.js applications.
27. Key Takeaways
Client Components are not a replacement for Server Components.
They are a tool for adding browser-side interactivity where it is actually needed.
Remember:
1Server Component 2 ↓ 3Default in App Router 4 ↓ 5Data + database + secrets + server logic 6 7Client Component 8 ↓ 9"use client" 10 ↓ 11State + effects + events + browser APIs
The most important principle is:
Use Client Components for interaction, not by default for entire pages.
A well-designed Next.js application often looks like:
1 Page 2 │ 3 Server Component 4 │ 5 ┌────────────┼────────────┐ 6 │ │ │ 7 Data Content Server Logic 8 │ 9 ▼ 10 Client Components 11 │ 12 ┌────┼────┬────┐ 13 │ │ │ │ 14 State Events Effects Browser APIs
This hybrid architecture lets Next.js keep as much work as possible on the server while sending only the JavaScript necessary for interactive parts of the application.