Home/Learn/Next.js/Server and Client Components

Server and Client Components

Advanced
Foundations

Everything is a server component until a file says otherwise. Knowing exactly what "use client" does — and what it does not — is the single most important thing in the App Router.

Overview

This is where most Next.js confusion lives, and almost all of it dissolves with one correction: `use client` does not mean "this renders in the browser instead of on the server". It means "this component and everything it imports is included in the client bundle, and it hydrates". Client components still render on the server for the initial HTML. The boundary is not about where code runs once — it is about what gets sent. Getting this right decides your bundle size, your secrets and whether the page works with JavaScript disabled.

What the Directive Actually Does

The boundary, and the fact that it is inherited.

It marks a bundle boundary, not a runtime
'use client'      // at the TOP of a file, before imports

// It means: this module and every module it imports goes into the
// client bundle. It does NOT mean "does not run on the server" —
// a client component still renders on the server for the initial
// HTML, then hydrates in the browser.

// The boundary is inherited DOWNWARD through imports:
// ClientThing.tsx has 'use client'
//   -> everything it imports is client too, even without the directive
// So one 'use client' at the top of a tree pulls the whole tree in.

// Server components cannot:
//   useState  useEffect  useRef  useContext
//   onClick and any event handler
//   window  document  localStorage
//   any browser-only library

// Client components cannot:
//   be async / await data directly
//   import server-only code (a database client, a secret)
//   read the filesystem

// The error that teaches this:
//   "You're importing a component that needs useState. It only works
//    in a Client Component, but none of its parents are marked with
//    'use client'."

Keeping the Boundary Low

The pattern that keeps pages fast: push "use client" to the leaves.

Push the directive to the leaves; pass children through
// Wrong — one interactive button turns the whole page into a client
// component, so the article body, the markdown parser and the data
// all ship to the browser.
'use client'
export default function ProblemPage({ problem }) {
  const [open, setOpen] = useState(false)
  return (<article>{problem.body}<button onClick={...}/></article>)
}

// Right — the page stays a server component; only the button is client
export default async function ProblemPage({ params }) {
  const problem = await getProblem(params.slug)
  return (
    <article>
      <MarkdownBody source={problem.body} />     {/* server: parser stays out
                                                     of the bundle */}
      <BookmarkButton slug={problem.slug} />     {/* client: 2KB */}
    </article>
  )
}

// Passing server content INTO a client component: use children.
// The children are rendered on the server and passed as already-
// rendered output, so they do not become client components:
<ClientTabs>
  <ServerHeavyChart data={data} />      {/* stays server */}
</ClientTabs>
// This "children as a slot" trick is the main tool for keeping a
// client wrapper from swallowing the tree beneath it.

The Serialization Rule

What can cross from a server component into a client one.

Props are serialized — and visible in the HTML
// Props crossing the boundary are SERIALIZED. So these work:
//   strings, numbers, booleans, null, undefined
//   arrays, plain objects, Date, Map, Set, BigInt
//   JSX elements
//   Server Action functions (see the mutations concepts)

// And these do not:
<ClientThing onDone={() => save()} />       // a plain function
<ClientThing db={prismaClient} />           // a class instance
<ClientThing date={dayjs()} />              // a library object
// "Functions cannot be passed directly to Client Components"

// Anything that must be interactive has to be DEFINED in the client
// component, not handed to it.

// The security consequence, which matters more:
// props sent to a client component are embedded in the HTML payload
// and readable by anyone who views source.
<UserCard user={user} />        // if user carries passwordHash,
                                // you just published it
// Select the fields explicitly at the boundary:
<UserCard user={{ id: user.id, name: user.name }} />

// Guard the server side of the line with the server-only package:
import 'server-only'            // build fails if a client imports this

Key Points to Remember

  • 1"use client" marks a bundle boundary, not a runtime — client components still render on the server for the initial HTML
  • 2The directive is inherited through imports, so one at the top of a tree pulls the whole tree into the bundle
  • 3Push "use client" down to the leaves, and pass server-rendered content through as children
  • 4Props crossing into a client component must be serializable — functions and class instances cannot cross
  • 5Those props are embedded in the HTML, so select fields explicitly and use the server-only package to guard server modules

Interview Questions

Sign in to ask Aria
1

What does the "use client" directive actually do?

Hard
2

How do you keep an interactive widget from turning its whole page into a client component?

Hard
3

Why is passing a full user object to a client component a security concern?

Medium

Ask Aria about Server and Client Components

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…