In the Next.js App Router, a component’s location in the server–client boundary affects where its code runs, what APIs it can use, and what gets sent to the browser. This guide explains how to choose between Server Components and Client Components, pass data across the boundary, and fix common boundary errors.
Key Takeaways
- App Router pages and layouts are Server Components by default; use a Client Component when a feature needs interactivity, state, effects, or browser-only APIs.
- Put
"use client"at the top of a module that starts a client-side import boundary. Keep that boundary as small as practical. - Fetch data in a Server Component when it fits the request and rendering model; pass the resulting values to Client Components as React-serializable props.
- A Server Component can be rendered as a child or slot of a Client Component, but a Client Component module must not import a Server Component module.
The rendering boundary
In the Next.js App Router, components are Server Components by default. They render on the server, where they can keep server-only code out of the browser bundle. They are a good fit for pages that read data, render content, and do not need browser interaction.
A Client Component is a component whose module belongs to the client-side module graph. It can use React state and effects, respond to events, and access browser APIs. It is still pre-rendered to HTML on the initial request when applicable, then hydrated in the browser so its interactions work. “Client” does not mean “never rendered on the server.”
What "use client" does
Add the directive before imports in a module when that module needs client capabilities:
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((value) => value + 1)}>
Count: {count}
</button>
);
}
The directive marks a client entry point. Modules imported by that entry point are part of the client-side module graph too, so avoid importing large or server-only modules there. You do not need to add the directive to every file used beneath that boundary.
A Server Component can render a Client Component. The client boundary starts where the client entry point is used; it does not turn the entire application into client-rendered code.
Choose by capability, not by component size
Use a Server Component when a component mainly reads data or renders content and does not need client-only behavior. Use a Client Component when it must respond to user input, keep local interactive state, synchronize with an external browser system, or use a browser API.
| Need | Usually choose | Reason |
|---|---|---|
| Read data for a page or render static content | Server Component | Can perform the work on the server without putting that code in the client bundle. |
Use useState, useEffect, or event handlers | Client Component | These interactive features run in the browser. |
Read window, document, or browser storage | Client Component | Those APIs are available in the browser, not during server rendering. |
| Share data from a server-side read with an interactive control | Both | Fetch in a Server Component and pass the needed value to a Client Component. |
A component that uses a hook is not automatically a Client Component in every React environment, but in the Next.js App Router, hooks such as useState and useEffect that require client behavior belong in a Client Component. Server Components cannot use browser APIs or client-side state and effects.
Data fetching: keep the read on the server when it fits
An async Server Component can load data and render it without adding a client-side fetching effect. This example assumes getProduct is an application function that reads from a trusted data source and returns a product or null:
import AddToCartButton from "./AddToCartButton";
import { getProduct } from "@/lib/products";
export default async function ProductPage({ params }) {
const { id } = await params;
const product = await getProduct(id);
if (!product) {
return <p>Product not found.</p>;
}
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</main>
);
}
The interactive control can be a separate Client Component:
"use client";
import { useState } from "react";
export default function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button
onClick={() => {
// Replace with the application's authorized cart action.
setAdded(true);
}}
>
{added ? "Added" : `Add product ${productId} to cart`}
</button>
);
}
The example keeps the data read in the Server Component and puts only the interactive control in the client graph. In a real application, the cart operation must be implemented with appropriate authorization and error handling; changing local UI state alone does not add an item to a server-side cart.
Server-side data access is not automatically safe simply because it happens in a Server Component. Enforce authorization in the data-access or action layer, and do not pass secrets or unnecessary private data to a Client Component. Anything passed as a client prop may be available to the browser.
Passing data and composing across the boundary
Values passed from a Server Component to a Client Component must be serializable by React for the Server Components boundary. This is not the same as saying every value must be JSON: follow React’s supported serialization rules for the React and Next.js versions in use. A normal function defined in a Server Component cannot be passed as an event-handler prop to a Client Component. Use a supported Server Function pattern when a server operation must be invoked from the client, and apply the required security checks.
Composition lets a Server Component render content and pass it through a Client Component’s slot, commonly children. The Client Component can control its own interactive wrapper without importing the Server Component module:
// Server Component
import Expandable from "./Expandable";
import ArticleDetails from "./ArticleDetails";
export default function Page() {
return (
<Expandable>
<ArticleDetails />
</Expandable>
);
}
// Expandable.jsx — Client Component
"use client";
import { useState } from "react";
export default function Expandable({ children }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen((value) => !value)}>
{open ? "Hide" : "Show"} details
</button>
{open ? children : null}
</section>
);
}
Here, the Server Component page creates the ArticleDetails element and supplies it as a slot. Expandable does not import ArticleDetails into its client module. This pattern is useful when an interactive shell should contain server-rendered content.
Common boundary errors
Using a hook in a Server Component
Symptom: Next.js reports that a hook or event handler is not supported in a Server Component.
Fix: Move the interactive behavior into a Client Component and mark its entry module with "use client". Keep the surrounding page or layout on the server when it does not need client behavior.
Accessing window during rendering
Symptom: A server render fails because window or document is undefined, or the initial server HTML differs from the browser render.
Fix: Put browser-dependent behavior in a Client Component. If an operation must wait until the browser has mounted, run it in an effect and account for loading and error states. Do not read browser globals at module scope in code that can be evaluated during server rendering.
Importing a Server Component into a Client Component
Symptom: A client entry imports a module that relies on server-only APIs or data access.
Fix: Do not import the Server Component from the Client Component. Render it from a Server Component and pass the resulting element through a prop or slot, such as children, when the composition fits.
Passing a callback from server-rendered code
Symptom: A regular function passed as a prop across the boundary cannot be serialized.
Fix: Keep the event handler inside the Client Component, pass serializable data, or use a supported Server Function mechanism for a server operation. A Server Function is not a substitute for authorization; validate the caller and requested operation.
Adding "use client" too high in the tree
Symptom: More modules than necessary enter the client-side graph, increasing the amount of code the browser must load and execute.
Fix: Move the directive down to the smallest component that needs client capabilities. Keep data access and static composition in Server Components where appropriate.
Best Practices
- Start with Server Components and introduce a Client Component where interaction or browser access is actually needed.
- Treat
"use client"as a module-graph boundary, and review the imports beneath it. - Pass only the data the client needs, using values supported by React’s serialization rules.
- Use props and slots to compose server-rendered content into interactive client-side shells.
- Handle loading, empty, and error states in the component or route that owns the relevant work.
- Keep authorization and secret-bearing operations on trusted server paths; rendering on the server alone does not authorize access.
Conclusion
Choose Server Components for server-side data work and rendering; choose Client Components for hooks, browser APIs, and interaction. Keeping the boundary narrow—and passing data or rendered content across it deliberately—makes App Router code easier to reason about without moving the whole page into the client bundle.
References
- Next.js: Server and Client Components — App Router guidance on component types, client boundaries, and composition.
- React:
use client— React’s reference for marking client entry points and the client module graph. - React: Server Components — React’s overview of Server Component capabilities and usage.
