NextjsServer/Client Composition Patterns

Server/Client Composition Patterns

Server Components and Client Components can be freely mixed in the same tree, but the rules for how they can import each other only work in one direction. Understanding that direction — and the pattern for working around it — is essential for structuring an App Router application well.

The Core Rule

A Server Component can import and render a Client Component without any special handling. But the reverse is not true in the way you might expect.

Warning
A Client Component cannot import a Server Component directly. Once a file has 'use client' at the top, every component it statically imports becomes part of the client bundle too — and Server Components (which may use server-only APIs like direct database access) cannot run in the browser.

TSX
// This does NOT work as you'd expect:
'use client'
import ServerComponent from './ServerComponent' // becomes a client component too!

export default function ClientWrapper() {
  return (
    <div>
      <ServerComponent />
    </div>
  )
}

Importing ServerComponent from inside a 'use client' file doesn't error loudly — instead, Next.js treats it as a Client Component from that point on, silently losing any server-only capability it relied on.

The Fix: Pass Server Components as children

The way around this is to never import a Server Component from within a Client Component's own module. Instead, have a parent Server Component render both, passing the Server Component down as a children prop (or any other prop) into the Client Component.

TSX
// ClientWrapper.tsx
'use client'
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen((o) => !o)}>Toggle</button>
      {open && children}
    </div>
  )
}

TSX
// ServerComponent.tsx (a Server Component, fetches its own data)
export default async function ServerComponent() {
  const data = await fetchFromDatabase()
  return <p>{data.title}</p>
}

TSX
// page.tsx — a Server Component composing both
import ClientWrapper from './ClientWrapper'
import ServerComponent from './ServerComponent'

export default function Page() {
  return (
    <ClientWrapper>
      <ServerComponent />
    </ClientWrapper>
  )
}

This works because ServerComponent is imported and rendered by page.tsx — a Server Component — not by ClientWrapper itself. ClientWrapper only ever receives the already rendered result as children; it never needs to import the server-only code.

Worked Example: Interleaving

This pattern scales to arbitrarily interleaved trees. A Server Component can pass several different Server Components into different named slots of a Client Component.

TSX
// Tabs.tsx (Client Component - manages which tab is active)
'use client'
export function Tabs({ overview, activity }: { overview: React.ReactNode; activity: React.ReactNode }) {
  const [tab, setTab] = useState<'overview' | 'activity'>('overview')
  return (
    <div>
      <button onClick={() => setTab('overview')}>Overview</button>
      <button onClick={() => setTab('activity')}>Activity</button>
      {tab === 'overview' ? overview : activity}
    </div>
  )
}

// page.tsx (Server Component)
import { Tabs } from './Tabs'
import { OverviewPanel } from './OverviewPanel' // Server Component, fetches data
import { ActivityPanel } from './ActivityPanel'   // Server Component, fetches data

export default function ProjectPage() {
  return <Tabs overview={<OverviewPanel />} activity={<ActivityPanel />} />
}

Both panels are fetched and rendered on the server ahead of time, and the Client Component simply decides which pre-rendered result to display — no client-side data fetching needed for the tab switch itself.

Quick Reference
  • Server Component importing Client Component -> always fine.

  • Client Component importing Server Component directly -> effectively converts it to a Client Component.

  • Client Component receiving a Server Component as children/props from its parent -> fine, and the recommended pattern.

  • This pattern is often called the "slot" pattern in App Router discussions.

Note
This restriction only applies to component composition, not to data. A Server Component can always pass plain serializable data down as props to a Client Component — that's covered in the next lesson.
Tip
When designing a new piece of UI, decide early which parts truly need interactivity (state, effects, event handlers) and keep those as small, leaf-level Client Components. Let everything above them stay a Server Component composing things together.

This composition model is what lets Next.js apps ship less JavaScript overall — most of the tree stays server-rendered, and only the interactive slivers hydrate on the client, receiving server-rendered content around them for free.