Every component in the Next.js App Router starts as a Server Component. Adding the use client directive opts a file, and everything it imports and renders directly, into the client JavaScript bundle. The directive earns its place only when a component actually needs a browser-only capability, not whenever a piece of UI feels interactive.
Getting this wrong is a bundle size problem, not a style preference. A page marked as a Client Component for one button ships the JavaScript for every component that page imports, even the ones that never react to anything. The rules below cover when the directive is the right call and when it just adds weight.
Add it when the component uses state or an effect
State and effect hooks only work in Client Components. A Server Component that calls one of them fails at build time, because there is no client runtime on the server to hold that state between renders.
// app/ui/quantity-input.tsx
'use client'
import { useState } from 'react'
export default function QuantityInput({ initial }: { initial: number }) {
const [quantity, setQuantity] = useState(initial)
const onChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setQuantity(Number(e.target.value))
return <input aria-label="Quantity" type="number" value={quantity} onChange={onChange} />
}This component holds local state that changes as the user types, so it runs on the server once to produce the initial HTML, then hydrates and runs entirely in the browser after that. useState is the reason the directive is here, not the fact that the component renders an input.
Add it when the component handles a browser event
Click, change, and submit handlers all require the browser's event system, which only exists in a Client Component after hydration. A Server Component can render the markup for a button, but it cannot attach a handler that runs in response to a click.
// app/ui/copy-link-button.tsx
'use client'
export default function CopyLinkButton({ url }: { url: string }) {
return (
<button onClick={() => navigator.clipboard.writeText(url)}>
Copy link
</button>
)
}This button runs on the server for the first HTML paint, then on the client so the click handler works. Both the event handler and the clipboard call are client-only, which is why this file needs the directive.
Add it when the component reads a browser API
window, localStorage, and observer APIs like IntersectionObserver do not exist during a server render. Reading one of them outside a Client Component throws at build or render time, because the server has no browser global to read.
// app/ui/theme-toggle.tsx
'use client'
import { useEffect, useState } from 'react'
export default function ThemeToggle() {
const [theme, setTheme] = useState<string | null>(null)
useEffect(() => {
setTheme(localStorage.getItem('theme') ?? 'light')
}, [])
return <span>Theme: {theme ?? 'loading'}</span>
}The effect reads local storage after mount, once the component is running in the browser, which is why the read happens inside useEffect instead of during the render itself. This runs on the server first for a placeholder render, then again on the client where local storage actually exists.
Add it when a third-party library needs a browser environment
Some npm packages call a state or effect hook, or a browser API, internally without including their own directive. Importing one directly into a Server Component fails, because Next.js has no way to know the package needs a client environment.
// app/ui/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default CarouselWrapping the import in its own file gives the package a client boundary without spreading the directive anywhere else. A Server Component can now import this wrapper and render the carousel normally, since the client boundary already starts inside carousel.tsx.
Do not add it just to fetch data
A Server Component can call an async data function directly and await the result, so fetching is never a reason to add the client directive. Marking a data-fetching component as client-side forces the browser to request the data itself instead of receiving it already rendered, which is slower and ships more code for no benefit.
// app/orders/[id]/page.tsx
import { getOrder } from '@/lib/data'
export default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const order = await getOrder(id)
return <p>Order total: {order.total}</p>
}This page stays a Server Component because it only fetches and renders. It never touches state, an effect, or a browser API, so nothing here requires the client boundary. The order data, and any query or key getOrder uses internally, never reaches the browser.
Do not add it because a component sounds interactive
A component named Card, Panel, or ProductDetails is not automatically a candidate for the client directive. Check what the component's body actually does: if it only reads props and renders markup, it belongs on the server regardless of what its name implies.
| Component does | Needs use client |
|---|---|
| Renders text, props, and static markup | No |
| Calls an async function and awaits data | No |
| Calls useState, useReducer, or useEffect | Yes |
| Attaches an event handler like onClick | Yes |
| Reads window, localStorage, or a browser-only API | Yes |
A component can look identical in the browser whether it is a Server Component or a Client Component. The directive is about what the code does, not what the rendered output looks like.
Do not mark a whole layout or page just because one child needs it
This is the most common and the most expensive mistake. Marking a page or layout as a Client Component drags every component it imports into the client bundle, including static ones that never needed to leave the server.
// app/products/[id]/page.tsx
'use client'
import { useState } from 'react'
import ProductDetails from '@/app/ui/product-details'
export default function ProductPage() {
const [added, setAdded] = useState(false)
return (
<div>
<ProductDetails />
<button onClick={() => setAdded(true)}>{added ? 'Added' : 'Add to cart'}</button>
</div>
)
}The only reason this page needs the directive is the state call for the add-to-cart button. Marking the whole page client-side means ProductDetails also ships as client JavaScript and loses the ability to fetch data directly on the server, even though it uses no state, effect, or event handler of its own.
Move the interactive piece into its own small file instead, and keep the page as a Server Component that fetches data and composes everything together.
// app/products/[id]/page.tsx
import ProductDetails from '@/app/ui/product-details'
import AddToCartButton from '@/app/ui/add-to-cart-button'
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
return (
<div>
<ProductDetails id={id} />
<AddToCartButton productId={id} />
</div>
)
}The page runs only on the server now, so ProductDetails can fetch its own data directly and ship no extra JavaScript. Only add-to-cart-button.tsx needs the client directive, since that is the one file with state and a click handler.
// app/ui/add-to-cart-button.tsx
'use client'
import { useState } from 'react'
export default function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false)
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added' : 'Add to cart'}
</button>
)
}This version ships less code because the client bundle for this route now only includes one small button component instead of the entire page tree. ProductDetails keeps direct server-side data access. The browser also has less JavaScript to download, parse, and run before the page becomes interactive.
Why over-marking has a real cost
Every component that sits below a client boundary ships its own JavaScript to the browser, whether or not that specific component uses any client-only feature. That JavaScript has to download, parse, and execute before those components finish hydrating, which adds real time on slower connections and devices.
Components below the boundary also lose direct server-side access to databases, file systems, and secrets. A component that could have called its data function directly now has to receive that data as a prop from something further up the tree, which pushes work and complexity onto the parent for no benefit.
When to use it
Add the client directive only on the smallest file where a component's own code calls a hook, attaches an event handler, or reads a browser API. Keep everything else, including the pages and layouts that render that component, as Server Components.
For the exact rules on what the directive marks and where the boundary sits in the module graph, see The use client Directive Explained. For the full comparison of what each component type can and cannot do, see Server Components vs Client Components in Next.js. For patterns that keep a Server Component rendering even when it sits visually inside a Client Component, see Composition Patterns for Server and Client Components. For a closer look at browser-only globals specifically, see Why window and localStorage Do Not Exist in Server Components.
Rune AI
Key Insights
- Add use client only when a component uses state, an effect, an event handler, or a browser API.
- Fetching data is never a reason to add use client, since Server Components can fetch directly.
- Every component below a use client boundary ships its JavaScript to the browser, even the static ones.
- Marking a whole page or layout client-side because one child is interactive is the most common and costly mistake.
- Move the directive down to the smallest leaf component and pass Server Components into it as children or props.
- Wrap third-party libraries that use hooks or browser APIs in a small Client Component file instead of marking the page that renders them.
Frequently Asked Questions
Does fetching data ever require use client?
If a component only calls fetch and renders text, does it need use client?
Can I put use client on a layout to make one child interactive?
How do I know if a third-party component needs a use client wrapper?
Conclusion
Add use client only to the file where a component genuinely needs state, an effect, an event handler, or a browser API, and nowhere higher than that. Every layer above the boundary should stay a Server Component so it keeps direct data access and ships no extra JavaScript. The habit that keeps a Next.js app fast is pushing the directive down to the smallest leaf that needs it, not up to the nearest page or layout.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.