Client Components
Add interactivity with "use client" -- hooks, event handlers, and browser APIs.
Search across all documentation pages
Add interactivity with "use client" -- hooks, event handlers, and browser APIs.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card -- copy-paste ready.
// app/components/counter.tsx
"use client";
import { useState } from "react";
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}// app/page.tsx (Server Component imports the Client Component)
import { Counter } from "./components/counter";
export default function Page() {
return (
<main>
<h1>Welcome</h1>
<Counter initialCount={5} />
</main>
);
}When to reach for this: You need useState, useEffect, useRef, event handlers (onClick, onChange), or browser APIs (window, localStorage, IntersectionObserver).
// app/components/search-autocomplete.tsx
"use client";
import { useState, useEffect, useRef, useTransition } from "react";
import { useRouter } from "next/navigation";
type Suggestion = { id: string; label: string };
export function SearchAutocomplete() {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const inputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
useEffect(() => {
if (query.length < 2) {
setSuggestions([]);
return;
}
const controller = new AbortController();
fetch(`/api/suggestions?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((res) => res.json())
.then((data) => setSuggestions(data))
.catch(() => {}); // ignore abort errors
return () => controller.abort();
}, [query]);
function handleSelect(suggestion: Suggestion) {
setQuery(suggestion.label);
setIsOpen(false);
startTransition(() => {
router.push(`/search?q=${encodeURIComponent(suggestion.label)}`);
});
}
return (
<div className="relative w-full max-w-md">
<input
ref={inputRef}
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value);
setIsOpen(true);
}}
onFocus={() => setIsOpen(true)}
placeholder="Search..."
className="w-full border rounded px-4 py-2"
/>
{isOpen && suggestions.length > 0 && (
<ul className="absolute top-full left-0 right-0 bg-white border rounded-b shadow-lg z-10">
{suggestions.map((s) => (
<li key={s.id}>
<button
onClick={() => handleSelect(s)}
className="w-full text-left px-4 py-2 hover:bg-gray-100"
>
{s.label}
</button>
</li>
))}
</ul>
)}
{isPending && (
<span className="absolute right-3 top-2.5 text-sm text-gray-400">
Loading...
</span>
)}
</div>
);
}// app/search/page.tsx (Server Component)
import { SearchAutocomplete } from "@/app/components/search-autocomplete";
export default function SearchPage() {
return (
<main className="p-6">
<h1 className="text-2xl font-bold mb-4">Search</h1>
<SearchAutocomplete />
</main>
);
}What this demonstrates:
"use client" directive at the top of the fileuseState, useEffect, useRef, useTransitiononChange, onFocus, onClickAbortControlleruseRouter for programmatic navigation"use client" at the top of a file marks it as a client boundary. The component and all its imports are included in the client JavaScript bundle."use client" directive applies to the file, not a single component. All exports from that file become Client Components."use client" in the child). They cannot import Server Components directly.Controlled form input:
"use client";
import { useState } from "react";
export function EmailForm({ onSubmitAction }: { onSubmitAction: (email: string) => Promise<void> }) {
const [email, setEmail] = useState("");
return (
<form action={async () => { await onSubmitAction(email); }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Subscribe</button>
</form>
);
}Using browser APIs safely:
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <p>Window: {size.width} x {size.height}</p>;
}Wrapping a third-party client library:
// app/components/map.tsx
"use client";
import { MapContainer, TileLayer, Marker } from "react-leaflet";
export function Map({ lat, lng }: { lat: number; lng: number }) {
return (
<MapContainer center={[lat, lng]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker position={[lat, lng]} />
</MapContainer>
);
}// Props from server must be serializable
type ClientProps = {
initialData: string[]; // OK
count: number; // OK
serverAction: (id: string) => Promise<void>; // OK (Server Action)
// onClick: () => void; // NOT OK (regular function)
// ref: React.Ref<T>; // NOT OK (not serializable)
};
// Event handler types
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { ... }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { ... }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }"use client" does not mean "client-only" -- Client Components are still server-rendered (SSR) on the initial request. They run on both server and client. Fix: If you need truly client-only rendering, use dynamic(() => import("..."), { ssr: false }).
Hydration mismatches -- If the server-rendered HTML differs from the client render (e.g., using Date.now(), Math.random(), or window checks), React logs a hydration error. Fix: Use useEffect for values that differ between server and client, or suppressHydrationWarning for intentional mismatches.
Importing a Server Component into a Client Component -- This is not allowed. The import will be treated as a Client Component. Fix: Pass the Server Component as children or another JSX prop instead.
Bundle size creep -- Everything imported into a "use client" file ends up in the client bundle, including utility libraries. Fix: Keep "use client" files small and focused. Import heavy libraries only where needed.
All exports become client components -- If you export both a utility function and a component from a "use client" file, the utility also becomes client-only. Fix: Keep utilities in separate, non-client files.
Hooks cannot be conditional -- React hooks must be called in the same order every render. Fix: Never put hooks inside if blocks, loops, or early returns.
| Approach | Use When | Don't Use When |
|---|---|---|
| Client Components | Interactive UI with state, effects, or browser APIs | Pure data display with no interactivity |
| Server Components | Read-only rendering, data fetching, no JS shipped | You need hooks or event handlers |
dynamic(import, { ssr: false }) | Component must never render on the server (e.g., canvas libs) | SSR is fine and you just need hydration |
| Web Components | You need framework-agnostic interactive elements | React components work for your use case |
Server Actions (form action) | You can handle the interaction with a form submission | You need real-time client-side feedback |
No. Client Components are server-rendered on the initial page load (SSR). The server sends HTML, then React hydrates it on the client to attach event listeners and make it interactive.
Use a Client Component when you need:
useState, useEffect, useRef, useTransition)onClick, onChange, onFocus)window, localStorage, IntersectionObserver)Wrap browser API usage in useEffect so it only runs on the client after hydration:
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <p>Width: {width}</p>;
}No. Importing a Server Component inside a "use client" file silently converts it into a Client Component. Instead, pass the Server Component as children or another JSX prop from a Server Component parent.
Because "use client" pulls all imports in that file into the client bundle, including utility libraries. Fix this by keeping "use client" files small and focused -- extract heavy logic into separate non-client files.
Hydration errors occur when server-rendered HTML differs from the client render. Common causes:
Date.now() or Math.random() during renderwindow properties during render instead of in useEffectuseEffect, or use suppressHydrationWarning for intentional mismatches.Use next/dynamic with ssr: false:
import dynamic from "next/dynamic";
const ClientOnlyMap = dynamic(
() => import("./map"),
{ ssr: false }
);function handleClick(e: React.MouseEvent<HTMLButtonElement>) { }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { }Server Actions are typed as async functions returning a Promise:
type FormProps = {
onSubmitAction: (email: string) => Promise<void>;
};Regular functions (non-Server Actions) are not valid as serializable props.
AbortController cancels in-flight fetch requests when the useEffect cleanup runs (e.g., when query changes)..catch(() => {}) ignores the abort error.useTransition lets you mark a state update as non-urgent so it does not block the UI.isPending is true while the transition is running, which is useful for showing loading indicators.router.push() to avoid blocking the input while navigating.useSearchParams in Client ComponentsReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥