Composition Patterns for Server and Client Components

A handful of concrete composition patterns keep Server and Client Components working together: leaf components, passed-down children, container splits, and scoped providers.

7 min read

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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.

SignalWhat it means
Only one part of the page needs state, an event, or a browser APIGive 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 wizardThe 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.tsxApp.tsx
// 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.

PatternUse it when
Leaf componentsOne small piece of a mostly static page needs interactivity
Children or propsA Client Component needs to display server-fetched content it did not fetch itself
Container and interactive splitOne feature both loads data and reacts to input
Scoped providerClient 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does composing a Server Component inside a Client Component send its source code to the browser?

No. The Server Component renders on the server first, and the Client Component only receives its finished output as serialized data, never the module that produced it.

Should every interactive piece of a page get its own Client Component file?

Yes, in most cases. A small, dedicated file keeps the client bundle limited to the code that actually needs state, events, or browser APIs, instead of pulling everything around it along too.

Can a Server Component and a Client Component live in the same file?

No. The client directive marks the whole file, so a file that has it cannot also export a Server Component. Split them into separate files and compose them from a parent.

Where should a context provider sit in the tree?

As deep as it can go while still wrapping every consumer, not at the very top of the layout. Wrapping only the branch that needs the context keeps more of the surrounding tree eligible for server rendering.

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.