React Server Components 10 🧬 Children as Slots — Passing Server-rendered JSX into Client wrappers
The children prop is the mechanism that lets a Client Component display server-rendered content without importing it. A Server Component renders the content, serializes the resulting JSX into the RSC Payload, and passes it to the Client Component as a prop. The Client Component receives React elements—not the Server Component’s code—and places them wherever its layout requires. This is how a modal with useState can contain a cart that fetches from the database, and how a tabbed interface with client-side state can contain panels that render server data.
This chapter covers the children slot pattern in detail: why it works, what it can carry, how it differs from importing, and the composition patterns it enables. It also covers the performance consequence—the Server Component’s code stays out of the client bundle—and the constraints that determine what can and cannot be passed through the slot.
Key point: children is a serialized React element. The Server Component that created it runs on the server. The Client Component that receives it runs on the client. The two are connected by the RSC Payload, not by an import.
Why children works across the boundary
The serialization mechanism. When a Server Component renders a Client Component, it passes props through the RSC Payload. The payload is a serialized representation of the React tree. JSX elements are part of that serialization—they are React elements with a type, props, and children. A Server Component’s output is a JSX element, and that element can be passed as a prop to a Client Component .
The module graph separation. The Server Component that creates the JSX is in the server module graph. The Client Component that receives it is in the client module graph. The two graphs do not import each other in this direction. The Client Component never sees the Server Component’s module. It receives the serialized element, which the client renderer can render because the element’s type is either a host component (div, span) or a Client Component that is already in the client bundle .
The layout problem. A Client Component often needs to wrap content in a layout: a modal with a header and footer, a collapsible panel with a toggle, a tabbed container with tab buttons. The content inside the layout may need server data. The children slot lets the Client Component own the layout and the interactivity, while the Server Component owns the data and the content. Neither needs to import the other .
a. The basic children pattern
The Client Component declares a children prop of type React.ReactNode and renders it somewhere in its output. The Server Component passes the server-rendered content as the child.
// app/ui/accordion.tsx (Client Component)
'use client'
import { useState } from 'react'
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>{title}</button>
{open && <div>{children}</div>}
</div>
)
}
// app/page.tsx (Server Component)
import { Accordion } from './ui/accordion'
import { OrderHistory } from './ui/order-history' // Server Component
export default async function Page() {
return (
<Accordion title="Order History">
<OrderHistory />
</Accordion>
)
}
Accordion is a Client Component. It owns the open state and the toggle button. OrderHistory is a Server Component. It fetches the order data on the server and renders the list. Page imports both, renders OrderHistory on the server, and passes the resulting JSX to Accordion as children. The OrderHistory code never enters the client bundle .
b. Named slots and multiple children
A Client Component can define multiple slot props, not just children. A layout with a header, sidebar, and main content can accept three separate props, each carrying a different server-rendered fragment.
// app/ui/three-column.tsx (Client Component)
'use client'
export function ThreeColumn({
header,
sidebar,
children,
}: {
header: React.ReactNode
sidebar: React.ReactNode
children: React.ReactNode
}) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
)
}
// app/page.tsx (Server Component)
import { ThreeColumn } from './ui/three-column'
import { Header } from './ui/header'
import { Sidebar } from './ui/sidebar'
import { Content } from './ui/content'
export default function Page() {
return (
<ThreeColumn
header={<Header />}
sidebar={<Sidebar />}
>
<Content />
</ThreeColumn>
)
}
Each slot receives a separately rendered Server Component. The Client Component does not know or care what each fragment contains. It only knows where to place them. This is the same pattern as a layout component that accepts named slots, and it works because each slot is a serialized JSX element .
c. The performance consequence
The children slot is not just a composition convenience. It is a bundle-size decision. When a Server Component is passed as children, its code and its dependencies stay on the server. When the same component is imported directly into a Client Component, its code enters the client bundle.
Consider a chart library that is large and only needs to render static data. If the chart is a Server Component rendered inside a Client Component’s children slot, the chart library stays on the server. If the chart is imported into the Client Component and rendered there, the entire library is bundled for the browser.
// Wrong: chart library enters the client bundle
'use client'
import { HeavyChart } from './heavy-chart' // Server Component, but imported
// Correct: chart stays on the server
// page.tsx
<ClientWrapper>
<HeavyChart /> {/* rendered on server, passed as children */}
</ClientWrapper>
The React documentation makes this point explicitly: Server Components can be passed as props to Client Components, and this is how a Client Component can render server content without importing it. The server content’s dependencies are not included in the client bundle .
Complete Example Session
// ============================================
// PART 1: THE CLIENT WRAPPER
// ============================================
// app/ui/tabs.tsx
'use client'
import { useState } from 'react'
export function Tabs({ tabs, children }: { tabs: string[]; children: React.ReactNode }) {
const [active, setActive] = useState(0)
return (
<div>
<div role="tablist">
{tabs.map((tab, i) => (
<button key={tab} onClick={() => setActive(i)} aria-selected={i === active}>
{tab}
</button>
))}
</div>
<div role="tabpanel">{children}</div>
</div>
)
}
// Tabs owns the active state and the tab buttons.
// The panel content comes from the parent as children.
// ============================================
// PART 2: THE SERVER CONTENT
// ============================================
// app/ui/overview.tsx (Server Component)
export async function Overview() {
const stats = await getStats()
return <dl>{/* render stats */}</dl>
}
// app/ui/settings.tsx (Server Component)
export async function Settings() {
const config = await getConfig()
return <form>{/* render config */}</form>
}
// Both fetch data on the server.
// ============================================
// PART 3: COMPOSING IN A SERVER COMPONENT
// ============================================
// app/page.tsx (Server Component)
import { Tabs } from './ui/tabs'
import { Overview } from './ui/overview'
import { Settings } from './ui/settings'
export default function Page() {
return (
<Tabs tabs={['Overview', 'Settings']}>
<Overview />
<Settings />
</Tabs>
)
}
// Tabs is a Client Component.
// Overview and Settings are Server Components.
// Both are passed as children.
// ============================================
// PART 4: NAMED SLOTS
// ============================================
// app/ui/dashboard-layout.tsx
'use client'
export function DashboardLayout({
header,
sidebar,
children,
}: {
header: React.ReactNode
sidebar: React.ReactNode
children: React.ReactNode
}) {
return (
<div className="grid">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
)
}
// ============================================
// PART 5: PASSING NAMED SLOTS
// ============================================
// app/dashboard/page.tsx (Server Component)
import { DashboardLayout } from '../ui/dashboard-layout'
import { Header } from '../ui/header'
import { Sidebar } from '../ui/sidebar'
import { MainContent } from '../ui/main-content'
export default function Page() {
return (
<DashboardLayout
header={<Header />}
sidebar={<Sidebar />}
>
<MainContent />
</DashboardLayout>
)
}
// ============================================
// PART 6: THE WRONG APPROACH
// ============================================
// app/ui/tabs.tsx
'use client'
import { Overview } from './overview' // ❌ Server Component
import { Settings } from './settings' // ❌ Server Component
export function Tabs() {
return <div><Overview /><Settings /></div>
}
// Build error: Client Component cannot import Server Component.
// ============================================
// PART 7: THE CORRECT APPROACH
// ============================================
// app/ui/tabs.tsx
'use client'
export function Tabs({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
// app/page.tsx
<Tabs>
<Overview />
<Settings />
</Tabs>
// Overview and Settings are rendered on the server.
// ============================================
// PART 8: BUNDLE SIZE CONSEQUENCE
// ============================================
// HeavyChart is a Server Component.
// It imports a large charting library.
//
// Passed as children:
// <ClientWrapper><HeavyChart /></ClientWrapper>
// → chart library stays on the server
//
// Imported into client:
// 'use client'
// import { HeavyChart } from './heavy-chart'
// → chart library enters the client bundle
// ============================================
// PART 9: CONDITIONAL CHILDREN
// ============================================
// app/page.tsx (Server Component)
export default async function Page({ searchParams }: { searchParams: Promise<{ tab?: string }> }) {
const { tab } = await searchParams
return (
<Tabs tabs={['Overview', 'Settings']}>
{tab === 'settings' ? <Settings /> : <Overview />}
</Tabs>
)
}
// The Server Component decides which child to render.
// The Client Component receives the result.
// ============================================
// PART 10: THE COMPOSITION RULE
// ============================================
// Client Component owns: state, event handlers, layout
// Server Component owns: data fetching, static content
//
// children is the bridge.
// The Server Component renders the content.
// The Client Component places it.
The ten parts covered the client wrapper, the server content, composition in a server component, named slots, passing named slots, the wrong approach, the correct approach, the bundle-size consequence, conditional children, and the composition rule.
Quick Reference
Slot patterns
| Pattern | Syntax | Use case |
|---|---|---|
| Default slot | children | Single content region |
| Named slots | header, sidebar, etc. | Multiple content regions |
| Conditional slot | {condition ? <A /> : <B />} | Server decides content |
What can be passed
| Value | Can be passed |
|---|---|
| JSX element (Server Component) | Yes |
| JSX element (host element) | Yes |
| String, number | Yes |
| Plain object | Yes |
| Plain function | No |
| Server Function | Yes (as reference) |
Why use children
| Benefit | Detail |
|---|---|
| Bundle size | Server Component code stays on server |
| Data access | Server Component can fetch directly |
| Separation | Layout and interactivity in client, content in server |
| Composition | Client Component does not import server code |
Common Client Component layouts
| Component | Slots |
|---|---|
| Modal | children |
| Tabs | children |
| Accordion | children |
| Dashboard layout | header, sidebar, children |
| Card | header, body, footer |
Best Practices
✅ Do This:
// Pass Server Components as children
<ClientWrapper><ServerContent /></ClientWrapper> // ✅
// Use named slots for multi-region layouts
<Layout header={<Header />} sidebar={<Sidebar />}> // ✅
// Let the Server Component decide conditional children
{tab === 'settings' ? <Settings /> : <Overview />} // ✅
// Keep heavy dependencies in Server Components
<ClientWrapper><HeavyChart /></ClientWrapper> // ✅
❌ Don’t Do This:
// Import a Server Component into a Client Component
'use client'
import { ServerContent } from './server' // ❌
// Pass a plain function as children
<ClientWrapper>{() => <ServerContent />}</ClientWrapper> // ❌
// Force a Server Component into the client bundle
// by importing it into a client module // ❌
// Assume children is rendered on the client
// The JSX was rendered on the server // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build error importing Server Component | Client module imports server module | Pass as children from Server Component |
| Children render blank | The Server Component threw during render | Check server logs; the client receives nothing |
| Function passed as children | Functions are not serializable | Pass JSX, not a render function |
| Bundle size grows unexpectedly | Server Component imported into client module | Check the module graph; use children |
| Named slot type error | Slot prop typed as JSX.Element instead of ReactNode | Use React.ReactNode for slot props |
Real-World Examples
1. Modal with server content
<Modal>
<Cart />
</Modal>
2. Tabs with server panels
<Tabs tabs={['Overview', 'Settings']}>
<Overview />
<Settings />
</Tabs>
3. Named slots
<Layout header={<Header />} sidebar={<Sidebar />}>
<Content />
</Layout>
4. Conditional children
{tab === 'settings' ? <Settings /> : <Overview />}
5. Heavy library in server component
<ClientWrapper><HeavyChart /></ClientWrapper>
6. Client wrapper type
children: React.ReactNode
7. Multiple children
<Wrapper>{header}{content}{footer}</Wrapper>
8. Server function as slot content
<Form action={createPost}>...</Form>
9. Bundle preservation
// HeavyChart code stays on server when passed as children
10. Anti-pattern
'use client'
import { ServerContent } from './server' // ❌
Visual
The children slot flow
┌─────────────────────────────────────────────────────────────┐
│ CHILDREN SLOT FLOW │
│ │
│ Server Component (Page) │
│ │ │
│ ├── imports ClientWrapper (Client) │
│ │ │
│ └── imports ServerContent (Server) │
│ │ │
│ ▼ │
│ Server renders ServerContent ──► JSX │
│ │ │
│ ▼ │
│ JSX serialized into RSC Payload │
│ │ │
│ ▼ │
│ ClientWrapper receives JSX as children │
│ │ │
│ ▼ │
│ ClientWrapper renders children in its layout │
└─────────────────────────────────────────────────────────────┘
Named slots
┌─────────────────────────────────────────────────────────────┐
│ NAMED SLOTS │
│ │
│ <Layout │
│ header={<Header />} │
│ sidebar={<Sidebar />} │
│ > │
│ <Content /> │
│ </Layout> │
│ │
│ ClientWrapper receives: │
│ props.header = serialized <Header /> │
│ props.sidebar = serialized <Sidebar /> │
│ props.children = serialized <Content /> │
│ │
│ Each slot is placed independently. │
└─────────────────────────────────────────────────────────────┘
Bundle size comparison
┌─────────────────────────────────────────────────────────────┐
│ BUNDLE SIZE │
│ │
│ Imported into Client Component: │
│ 'use client' │
│ import { HeavyChart } from './heavy-chart' │
│ → HeavyChart + chart library in client bundle │
│ │
│ Passed as children: │
│ <ClientWrapper><HeavyChart /></ClientWrapper> │
│ → HeavyChart rendered on server │
│ → chart library stays on server │
└─────────────────────────────────────────────────────────────┘
Render tree vs module graph
┌─────────────────────────────────────────────────────────────┐
│ RENDER TREE MODULE GRAPH │
│ │
│ ClientWrapper page.tsx │
│ └── ServerContent ├── client-wrapper.tsx │
│ └── server-content.tsx │
│ │
│ ServerContent appears inside ClientWrapper does NOT │
│ ClientWrapper in the tree. import ServerContent. │
│ │
│ The slot is a serialization bridge, not an import. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Slot prop type | React.ReactNode |
| What crosses | Serialized JSX elements |
| What does not cross | Plain functions |
| Default slot | children |
| Named slots | Any prop typed as React.ReactNode |
| Bundle effect | Server Component code stays on server |
| Decision maker | The Server Component chooses what to render |
| Placement | The Client Component chooses where to render |
Key takeaways:
childrenis a serialization bridge. The Server Component renders the content to JSX, the framework serializes it into the RSC Payload, and the Client Component receives it as a prop. The Client Component never imports the Server Component’s code .- The Server Component decides the content; the Client Component decides the placement. This separation is the core of the composition model. The Server Component owns data fetching and rendering; the Client Component owns layout and interactivity .
- Named slots extend the pattern. Any prop typed as
React.ReactNodecan carry server-rendered content. A layout with header, sidebar, and main regions can accept three separate slots, each rendered by a different Server Component . - The bundle-size benefit is real. A heavy Server Component passed as
childrenstays on the server. The same component imported into a Client Component enters the client bundle. Thechildrenslot is a performance tool, not just a composition convenience. - Plain functions cannot be passed as children. A render function like
{() => <Content />}fails because functions are not serializable. The content must be JSX, not a function that returns JSX. - The pattern is the standard solution to the reverse-import limit. A Client Component cannot import a Server Component, but it can receive one as
children. This is how modals contain server data, tabs contain server panels, and interactive layouts contain server-rendered content.
Remember: The children slot is the answer to the question “how does a Client Component display server content?” The Server Component renders the content and passes the result as a prop. The Client Component places it in its layout. The two never import each other. The Server Component’s code stays on the server, and the Client Component’s code stays in the client bundle. Named slots extend the pattern to multiple regions. The only constraint is serialization: JSX crosses, functions do not. Use children to keep heavy server components out of the client bundle, and use named slots to build layouts that combine client-side interactivity with server-side data.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!