React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

client-componentsuse-clienthydrationinteractivityhooks

Client Components

Add interactivity with "use client" -- hooks, event handlers, and browser APIs.

Recipe

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).

Working Example

// 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:

  • The "use client" directive at the top of the file
  • Multiple hooks: useState, useEffect, useRef, useTransition
  • Event handlers: onChange, onFocus, onClick
  • Fetching data on the client with cleanup via AbortController
  • Using useRouter for programmatic navigation
  • A Server Component importing and rendering the Client Component

Deep Dive

How It Works

  • Adding "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.
  • Client Components are still server-rendered on the initial page load (SSR). The HTML is generated on the server, sent to the browser, and then hydrated -- React attaches event listeners and makes the component interactive.
  • During client-side navigation, Client Components render entirely on the client using the RSC payload from the server.
  • The "use client" directive applies to the file, not a single component. All exports from that file become Client Components.
  • Client Components can import other Client Components (with or without "use client" in the child). They cannot import Server Components directly.
  • Props passed from a Server Component to a Client Component must be serializable.

Variations

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>
  );
}

TypeScript Notes

// 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>) { ... }

Gotchas

  • "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.

Alternatives

ApproachUse WhenDon't Use When
Client ComponentsInteractive UI with state, effects, or browser APIsPure data display with no interactivity
Server ComponentsRead-only rendering, data fetching, no JS shippedYou 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 ComponentsYou need framework-agnostic interactive elementsReact components work for your use case
Server Actions (form action)You can handle the interaction with a form submissionYou need real-time client-side feedback

FAQs

What does the "use client" directive actually do?
  • It marks the file as a client boundary -- the component and all its imports are included in the client JavaScript bundle.
  • It applies to the entire file, not a single component.
  • Client Components are still server-rendered (SSR) for the initial HTML, then hydrated on the client.
Are Client Components rendered only in the browser?

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.

When should I use a Client Component instead of a Server Component?

Use a Client Component when you need:

  • React hooks (useState, useEffect, useRef, useTransition)
  • Event handlers (onClick, onChange, onFocus)
  • Browser APIs (window, localStorage, IntersectionObserver)
How do I safely access browser APIs like window or localStorage?

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>;
}
Can a Client Component import a Server Component?

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.

What props can be passed from a Server Component to a Client Component?
  • Props must be serializable: strings, numbers, booleans, arrays, plain objects, and Server Actions.
  • Regular functions, class instances, Refs, and Symbols are not serializable and will fail.
Gotcha: Why does adding "use client" to a file increase my bundle size significantly?

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.

Gotcha: I see a hydration mismatch error -- what causes it?

Hydration errors occur when server-rendered HTML differs from the client render. Common causes:

  • Using Date.now() or Math.random() during render
  • Reading window properties during render instead of in useEffect
  • Fix: Move differing values into useEffect, or use suppressHydrationWarning for intentional mismatches.
How do I make a component render only on the client with no SSR?

Use next/dynamic with ssr: false:

import dynamic from "next/dynamic";
 
const ClientOnlyMap = dynamic(
  () => import("./map"),
  { ssr: false }
);
How do I type event handlers in a Client Component with TypeScript?
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { }
How do I type props for a Client Component that receives a Server Action?

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.

What is the AbortController pattern shown in the search autocomplete example?
  • The AbortController cancels in-flight fetch requests when the useEffect cleanup runs (e.g., when query changes).
  • This prevents stale responses from overwriting newer results.
  • The .catch(() => {}) ignores the abort error.
What does useTransition do in a Client Component?
  • 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.
  • In the example, it wraps router.push() to avoid blocking the input while navigating.