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