| | |

React Server Components 10 🧬 Children as Slots — Passing Server-rendered JSX into Client wrappers

⏱️ Estimated read: 13 min | |

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

PatternSyntaxUse case
Default slotchildrenSingle content region
Named slotsheader, sidebar, etc.Multiple content regions
Conditional slot{condition ? <A /> : <B />}Server decides content

What can be passed

ValueCan be passed
JSX element (Server Component)Yes
JSX element (host element)Yes
String, numberYes
Plain objectYes
Plain functionNo
Server FunctionYes (as reference)

Why use children

BenefitDetail
Bundle sizeServer Component code stays on server
Data accessServer Component can fetch directly
SeparationLayout and interactivity in client, content in server
CompositionClient Component does not import server code

Common Client Component layouts

ComponentSlots
Modalchildren
Tabschildren
Accordionchildren
Dashboard layoutheader, sidebar, children
Cardheader, 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

PitfallWhy It HappensFix
Build error importing Server ComponentClient module imports server modulePass as children from Server Component
Children render blankThe Server Component threw during renderCheck server logs; the client receives nothing
Function passed as childrenFunctions are not serializablePass JSX, not a render function
Bundle size grows unexpectedlyServer Component imported into client moduleCheck the module graph; use children
Named slot type errorSlot prop typed as JSX.Element instead of ReactNodeUse 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

ItemValue
Slot prop typeReact.ReactNode
What crossesSerialized JSX elements
What does not crossPlain functions
Default slotchildren
Named slotsAny prop typed as React.ReactNode
Bundle effectServer Component code stays on server
Decision makerThe Server Component chooses what to render
PlacementThe Client Component chooses where to render

Key takeaways:

  • children is 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.ReactNode can 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 children stays on the server. The same component imported into a Client Component enters the client bundle. The children slot 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.

Was this helpful?


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!