skip to content

The 'use client' Boundary

'use client' does not mark one file as client code — it marks an entry point, and everything imported beneath it comes along. Interviewers ask where you'd place the directive, because putting it at the top of a layout ships your whole app to the browser.

part ofNext.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In the Next.js App Router, files under app/ render on the server unless told otherwise. Which of your components actually need a 'use client' directive, and which do not?

level: juniorimportance: must knowfreq 80%

answer

  1. default is server, opt in explicitly
  2. ask what forces the opt-in
  3. state, event handlers, browser globals
  4. markup and data fetching need nothing
  5. mark the smallest module, not the page

basics

~20 s

Only components that need browser behaviour: React state or effect hooks, DOM event handler props such as onClick, or browser APIs like window and localStorage. Everything else — markup, layout, data fetching — stays a Server Component with no directive.

solid answer

~40 s

In the App Router everything under `app/` is a Server Component by default, so the real question is what forces you out of that default. Three things do: hooks that need a live component instance (`useState`, `useEffect`, `useRef`, `useContext`), DOM event handler props like `onClick` or `onChange`, and browser-only APIs such as `window`, `localStorage` or `IntersectionObserver`. A fourth case is indirect — a dependency that does one of those internally without shipping its own directive. If a component only receives props, maps data to JSX, or awaits a query, it needs nothing. And because the directive applies to the module it sits in *and* everything that module imports, the practical skill is placing it on the smallest interactive file rather than on the page or layout above it.

code

tsx · 12 lines
tsx
'use client'

import { useState } from 'react'

export function LikeButton({ initial }: { initial: number }) {
  const [count, setCount] = useState(initial)
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {count} likes
    </button>
  )
}

go deeper

for a junior

Be able to say that App Router files are server-side by default and name the three triggers out loud: state or effect hooks, event handler props like onClick, and browser APIs such as window or localStorage.

for a middle

Explain why those three are the triggers — a server render happens once with no instance to hold state and no way to serialize a function — and why static, prop-driven components are fine on the server.

for a senior

Show the placement judgment: mark the smallest interactive module, keep pages and layouts on the server, and describe how you would diagnose a component that quietly ended up on the client.

for a principal

Own the direction of the default. Be ready to argue why the loud failure (server code touching the browser) is cheaper than the quiet one (silently widening the client boundary), and how you keep a team's habits pointed that way.

## The default you are opting out of In the App Router, every file under `app/` — pages, layouts, and the components they import — renders on the server unless some module in its import chain declares `'use client'`. That default is Next's decision, not React's: the Pages Router took the opposite position, where everything under `pages/` was client code that happened to be prerendered to HTML. So in App Router work the interesting question is never "how do I make this render on the server", it is "what forced this one to be client?" ## The three things that force the directive **1. Hooks that need a living component instance.** `useState`, `useReducer`, `useEffect`, `useLayoutEffect`, `useRef`, `useContext`, and any custom hook built from them. A Server Component renders once and is finished; there is no instance to hold state in or to re-run an effect against. `createContext` belongs in the same bucket — you cannot create or provide a context from a Server Component. **2. DOM event handler props.** `onClick`, `onChange`, `onSubmit`, `onMouseEnter` — anything that passes a function into a DOM element prop. The server render produces a serialized description of the UI, and a function has no representation in it, so Next fails the render with an error saying event handlers cannot be passed to Client Component props. **3. Browser-only APIs.** `window`, `document`, `localStorage`, `navigator`, `IntersectionObserver`, `addEventListener` — none of them exist in the Node process doing the render. There is a fourth, indirect case: a dependency that does any of the above internally. If the package ships its own `'use client'`, you can import it straight from a Server Component and the boundary begins inside the package; if it does not, you have to open the boundary yourself. ## What does not force it A surprising amount of an app. Mapping an array to JSX, conditional rendering, receiving props and rendering markup, importing a pure formatting helper, awaiting a database query or a `fetch`, reading `cookies()` or `headers()` from `next/headers` — that last one is the reverse case, since `next/headers` is server-only and importing it into a client module is an error. Presentation components are the common surprise: a card, a table row, a badge, a nav list built from `<Link>` — none of them need the directive. ## Placement matters more than the decision `'use client'` is written as the first statement of a module, above the imports, and it applies to that module *and* everything the module imports, transitively. That is why "which component needs it?" is really "which module do I write it in?" The directive on a small `like-button.tsx` costs you that button; the same directive on the page above it pulls every import of that page into the client graph. ```tsx // app/products/page.tsx — no directive, renders on the server import { LikeButton } from './like-button' // that file has 'use client' export default async function Page() { const items = await getProducts() return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <LikeButton initial={item.likes} /> </li> ))} </ul> ) } ``` ## Telling where a component actually ran In development, a Server Component's `console.log` appears only in the terminal running the dev server. A Client Component's appears in the browser devtools — and usually in the terminal too, because Next prerenders client components to HTML for the first paint. A log showing up in both places is a reliable tell that the module landed inside the client boundary. Deleting the directive and reading the error Next raises tells you exactly what forced it. ## The cost of being wrong in each direction Wrong toward the client is quiet: the page still works, it just ships more JavaScript and any server-side data access in that module has to move somewhere else. Wrong toward the server is loud: the render fails immediately with a message naming the hook or the handler. That asymmetry is the argument for keeping the default and opting out narrowly — the failure mode that shouts fixes itself, and the one that whispers is the one that accumulates.

  • A colleague says every component that ends up visible in the browser needs 'use client'. What is wrong with that?
    It confuses rendering with running. A Server Component's output is visible in the browser as HTML; the directive is about whether the component's own code ships and executes there. Static markup, lists, cards and data-fetching components are visible and still need nothing.
  • How would you check, without reading the code, whether a component ended up on the client?
    Put a `console.log` in it and run the dev server. A Server Component logs only in the terminal. A Client Component logs in the browser devtools, and typically in the terminal too because Next prerenders it for the first paint. Seeing it in both places is the tell.
  • A Server Component needs a Save button. Can it render any interactivity at all?
    It can render the `<button>` markup, but it cannot attach an `onClick`. The normal move is to extract just the button into its own small client module and render that from the server component, keeping the surrounding page on the server.

saying these in an interview costs you the question

  • Says every component needs 'use client' to appear in the browser
  • Adds the directive to the page to fix one button
  • Thinks 'use client' is required to receive props
  • Believes Server Components mean the page ships zero JavaScript
  • Cannot name a concrete thing that forces the directive

context

open as a page

In a Next.js App Router app, a component marked 'use client' reads process.env.API_TOKEN and logs undefined in the browser, while the identical read works inside a Server Component. Why, and what should the team do about it?

level: middleimportance: should knowfreq 52%

basics

~20 s

Next only inlines environment variables whose names start with NEXT_PUBLIC_ into browser code, as a build-time text substitution. Anything else is simply absent in the browser and reads as undefined. If the value is a secret, keep the work on the server and pass only the result.

open as a page

You import a UI component from an npm package into a Next.js App Router page and the build fails saying the component needs useState and only works in a Client Component — but the package ships no 'use client' directive of its own. How do you fix it without turning the page into a Client Component?

level: middleimportance: should knowfreq 48%

basics

~20 s

Create a thin module of your own that starts with 'use client' and re-exports the package's component, then import that module from the page. Your wrapper becomes the boundary, the package's code sits below it, and the page stays a Server Component.

open as a page

In a Next.js App Router app, a team adds 'use client' to the top of app/dashboard/layout.tsx. Which code actually crosses into the client boundary as a result, and do the pages rendered inside that layout become Client Components too?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The boundary follows the import graph, not the route tree. The layout module and everything it imports become client code; the pages nested under it do not, because Next renders them separately and passes their output in as the children prop.

open as a page