Composition patterns are the small set of ways a Next.js component tree mixes Server Components and Client Components without one absorbing the other. They matter because the two types cannot nest however you like: a Client Component cannot import a Server Component's module, and marking a file interactive pulls everything that file imports into the browser bundle with it.
A page can look correct and still ship far more JavaScript than it needs, because one interactive control dragged a whole layout across the boundary with it. The patterns below are the concrete ways to avoid that, each solving a different shape of the same problem.
// app/dashboard/notifications-menu.tsx
'use client'
import { useState } from 'react'
export default function NotificationsMenu() {
const [open, setOpen] = useState(false)
return (
<button aria-expanded={open} onClick={() => setOpen(!open)}>
Notifications
</button>
)
}This file is the only interactive piece on its page, so it is the only file that needs the client directive. Everything around it, including the page and layout that render it, can stay a Server Component, which is the outcome every pattern in this article is aiming for.
Keep interactive files small and push them to the leaves
The core rule behind every other pattern here is to mark the smallest possible file as a Client Component, as close to the actual control as it can go. Such a file pulls in everything it imports, so placing the directive high in the tree drags static content along with it for no reason.
// app/dashboard/layout.tsx
'use client'
import Sidebar from './sidebar'
import NotificationsMenu from './notifications-menu'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Sidebar />
<NotificationsMenu />
<main>{children}</main>
</div>
)
}Only the notifications menu needs state, but the directive sits on the layout file above, so the sidebar and every page rendered through children also become part of the client bundle. Moving the directive down fixes this without changing what the layout renders.
// app/dashboard/layout.tsx
import Sidebar from './sidebar'
import NotificationsMenu from './notifications-menu'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Sidebar />
<NotificationsMenu />
<main>{children}</main>
</div>
)
}The layout has no directive of its own now, so it stays a Server Component, and so does the sidebar. Only notifications-menu.tsx, a leaf of the tree, ships as browser code. The full rules for exactly what counts as a leaf are covered in Server Components vs Client Components in Next.js.
Pass Server Components down as children or props
A Client Component can still display a Server Component, as long as a Server Component above it does the rendering and hands the finished result down as a prop, instead of the Client Component importing it directly.
// app/page.tsx
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}Page and Cart are Server Components, and Modal is a Client Component that only manages open and closed state. Cart still fetches its own data on the server, because the modal file never imports it, it only places whatever arrives in that prop. This composition is common enough to deserve its own walkthrough, including how it looks from the receiving side: see Passing Server Components as Children to Client Components.
Split a Server container from a Client interactive piece
For a feature that both loads data and reacts to input, split it into two components instead of one. A Server container fetches the data and stays async, while a small Client component receives that data as a prop and owns the interactive state.
// app/orders/orders-container.tsx
import OrdersTable from './orders-table'
import { getOrders } from '@/lib/data'
export default async function OrdersContainer() {
const orders = await getOrders()
return <OrdersTable orders={orders} />
}This container runs only on the server. It queries the orders directly, with no route handler in between, then passes the plain array down as a prop once the query resolves, which is the entire job of this file.
// app/orders/orders-table.tsx
'use client'
import { useState } from 'react'
export default function OrdersTable({ orders }: { orders: { id: string; total: number }[] }) {
const [sorted, setSorted] = useState(false)
const rows = sorted ? [...orders].sort((a, b) => b.total - a.total) : orders
return (
<div>
<button onClick={() => setSorted(!sorted)}>Sort by total</button>
<ul>{rows.map((o) => <li key={o.id}>{o.id}: {o.total}</li>)}</ul>
</div>
)
}This table runs on the server once to produce the initial rows, then again in the browser after hydration, because it holds the sort state. Clicking the button re-sorts the array already in memory, with no new request back to the server, so the query and the sorting logic stay in separate files instead of one tangled component.
Do not mark a whole page or layout client-side for one small piece
A page or layout usually renders far more static content than interactive content, so putting the client directive on that top-level file to fix one control is the most common way a client bundle grows without anyone noticing. The fix is the same every time: give that one control its own file, and let the page compose it in as a normal import.
| Signal | What it means |
|---|---|
| Only one part of the page needs state, an event, or a browser API | Give that part its own file, keep the page a Server Component |
| Most of the page needs the same client state, such as a full client-side wizard | The page genuinely qualifies as a Client Component |
Checking this signal before reaching for the directive avoids the exact mistake shown in the dashboard layout example earlier. A page that reads mostly as static content, with one toggle or one field that changes, almost always belongs in the first row of that table. See When to Add use client and When Not To for a closer look at making that call.
Context providers need their own small client wrapper
React context is not supported inside a Server Component, so a provider always needs to live behind the client directive. That does not mean the whole tree above it has to become a Client Component too.
// app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext('light')
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}A Server Component, such as a layout, can still import and render this provider directly, wrapping only the branch of the tree that actually reads the context rather than the entire document. Keeping the provider this narrow means the rest of the layout around it stays eligible to render on the server. The full pattern, including passing server-fetched data through a provider, has its own planned article: Context Providers in the App Router.
Which pattern to reach for
Most real pages combine more than one of these patterns at once.
| Pattern | Use it when |
|---|---|
| Leaf components | One small piece of a mostly static page needs interactivity |
| Children or props | A Client Component needs to display server-fetched content it did not fetch itself |
| Container and interactive split | One feature both loads data and reacts to input |
| Scoped provider | Client Components across the tree need to read shared state |
Start every new piece of UI as a Server Component, and only reach for one of these patterns once a build error or a real interactivity requirement forces the question. That habit keeps the client bundle limited to the pieces of the page that genuinely need to run in the browser.
Rune AI
Key Insights
- Interactive files should be small and placed as far down the tree as the behavior requires.
- A Server Component can be composed inside a Client Component through children or another prop without joining the client bundle.
- Splitting a Server container that fetches data from a small Client piece that handles interaction keeps most of a feature on the server.
- Marking a whole page or layout client-side because one control needs state pulls everything that file imports into the browser bundle.
- Context providers must live behind the client directive, but they should wrap only the branch of the tree that reads the context.
- Composition patterns are how Server and Client Components share one tree without either one absorbing the other.
Frequently Asked Questions
Does composing a Server Component inside a Client Component send its source code to the browser?
Should every interactive piece of a page get its own Client Component file?
Can a Server Component and a Client Component live in the same file?
Where should a context provider sit in the tree?
Conclusion
Composition patterns are what let a Next.js app stay mostly server rendered while still supporting real interactivity. Push interactive files to the leaves of the tree, hand Server Components down through children and props instead of importing them, split a data-fetching container from its interactive piece, and scope providers to the branch that actually needs them.
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.