Composition Patterns for Server and Client Components

Intermediate
12 min

Composition Patterns for Server and Client Components

Knowing the difference between Server and Client Components is half the job; the other half is arranging them so the client bundle stays small and server code never leaks into the browser. After this lesson you will be able to place "use client" boundaries deliberately, pass Server Components through Client Components, and guard modules with server-only.

The Boundary Is About Imports, Not Files

"use client" marks the entry point of a client subtree. Every module a client file imports becomes client code too, even if it never uses a hook. The reverse is fine: a Server Component may import and render a Client Component freely.

| Pattern | Works? | Why | |---|---|---| | Server imports Client | Yes | Rendered on the server, JS shipped to the browser | | Client imports Server | No | The import turns it into a Client Component | | Client receives Server as children | Yes | The server renders it first and passes the result |

Rule of thumb: push "use client" as far down the tree as possible. Pages stay Server Components; only the interactive leaf (a button, a dropdown, a form) becomes a Client Component.

Passing Server Components Through Client Components

A Client Component cannot import a Server Component, but it can receive one as a prop, already rendered by the server:

tsx
// components/Collapsible.tsx "use client"; import { useState } from "react"; export function Collapsible({ title, children }: { title: string; children: React.ReactNode }) { const [open, setOpen] = useState(false); return ( <section> <button onClick={() => setOpen(!open)}>{title}</button> {open && children} </section> ); }
tsx
// app/report/page.tsx (Server Component) import { Collapsible } from "@/components/Collapsible"; import { HeavyTable } from "./HeavyTable"; // async Server Component export default function ReportPage() { return ( <Collapsible title="Show details"> <HeavyTable /> </Collapsible> ); }

HeavyTable runs on the server with full database access; only its rendered output crosses the boundary. The same technique lets a client-side Providers wrapper in the root layout contain server-rendered pages, as in the sample at the top of this lesson.

Props Must Be Serializable

Anything a Server Component passes to a Client Component travels in the RSC payload, so it must be serializable: primitives, plain objects, arrays, Date, Map, Set, promises and JSX. Functions are not, with one exception: Server Actions marked "use server". If a Client Component needs a callback, define it inside that component or make it a Server Action.

Guarding Server-Only Modules

A data-access file that reads secrets must never be imported from client code. Enforce that at build time with the server-only package (npm install server-only):

typescript
// lib/data.ts import "server-only"; export async function getUsers() { const res = await fetch(process.env.API_URL + "/users", { headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }, }); return res.json(); }

If any "use client" file imports lib/data.ts, the build fails instead of shipping your token to the browser. The companion client-only package does the opposite for code that touches window.

Third-Party Components Without "use client"

Some npm packages ship hook-based components without the directive, so importing them in a Server Component fails. Re-export them once from a client file:

tsx
// components/Carousel.tsx "use client"; export { Carousel } from "acme-carousel";

Common mistakes

  • Adding "use client" to a layout or page for one hook, turning the whole subtree into client code.
  • Importing a heavy library into a shared file that a client file also imports, so it ships to the browser.
Quick Quiz
Question 1 of 3

A Client Component imports a Server Component directly. What happens?

Key Takeaways

  • "use client" is an import-graph boundary: everything a client file imports becomes client code.
  • Keep pages and layouts on the server; mark only interactive leaves as client.
  • Pass Server Components into Client Components as children or props, never by import.
  • Props crossing the boundary must be serializable; functions are allowed only as Server Actions.
  • Use server-only to guarantee secrets and database code never reach the browser.

Next lesson: Data Fetching and Caching — fetch data directly in Server Components and control how Next.js caches it.

Composition Patterns for Server and Client Components - Next.js | CodeYourCraft | CodeYourCraft