| | |

React Server Components 08 🧬 Composition Basics — Importing Client Components from Server Components

⏱️ Estimated read: 12 min | |

The primary direction of composition in the RSC model is Server Component importing Client Component. This is the normal, expected path: a server module renders the page, fetches data, and includes interactive Client Components at the points where the UI needs state or event handlers. The reverse direction—Client Component importing Server Component—is not allowed, because the client cannot evaluate server code. Composition is how the two environments cooperate without violating that boundary.

This chapter covers the import direction, the serializable props that cross the boundary, the children slot pattern that lets a Client Component render server-rendered UI, and the bundle-size discipline of pushing the client directive down to the smallest interactive unit.

Key point: A Server Component can import and render a Client Component. A Client Component cannot import a Server Component. But a Client Component can receive a Server Component as children if a Server Component rendered it first.


Why the import direction is one-way

The module graph rule. When a server module imports a "use client" module, the bundler creates a reference. The Client Component’s code is included in the client bundle, and the server renders it to HTML as part of the initial page. The client then hydrates it. The import is legal because the Client Component is designed to run in both environments: server-rendered to HTML, client-hydrated for interactivity .

When a client module imports a server module, the bundler would need to include the server module’s code in the client bundle. But server modules can contain database queries, filesystem access, and secrets. Including them in the client bundle would leak those resources and fail to run in the browser. The framework rejects the import at build time .

The serialization rule. When a Server Component renders a Client Component, the props it passes must be serializable. The server serializes the props into the RSC Payload, and the client deserializes them before rendering the Client Component. Primitives, plain objects, arrays, Date, JSX elements, and Server Functions all serialize. Plain functions and class instances do not .

The entry-point rule. Only the file that is imported by a Server Component needs the "use client" directive. A Client Component can import other Client Components without repeating the directive, because the boundary is already established. The directive marks the entry point, not every file in the client subtree .


a. Importing a Client Component

A Client Component is defined in a file with "use client" at the top. A Server Component imports it like any other component and renders it with serializable props.

// app/ui/like-button.tsx
'use client'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
  return <button onClick={() => setLikes(likes + 1)}>{likes} likes</button>
}
// app/page.tsx (Server Component, no directive)
import LikeButton from './ui/like-button'

export default async function Page() {
  const post = await getPost()
  return (
    <article>
      <h1>{post.title}</h1>
      <LikeButton initialLikes={post.likes} />
    </article>
  )
}

The Page component is a Server Component. It fetches the post data on the server. The LikeButton is a Client Component. It receives initialLikes as a number, which is serializable. The button’s code ships to the client, and the useState hook works because the component runs in the browser after hydration .

The props must be serializable. Passing onClick={handleClick} from the Server Component would fail, because handleClick is a plain function. The Client Component must define its own event handler internally, or receive a Server Function (marked with "use server") as a prop reference .


b. The children slot pattern

A Client Component cannot import a Server Component, but it can receive one as children. The Server Component renders the server content first, passes the resulting JSX to the Client Component, and the Client Component places it in its layout. The Server Component’s code never enters the client bundle.

// app/ui/modal.tsx
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>
}
// app/page.tsx (Server Component)
import Modal from './ui/modal'
import Cart from './ui/cart' // Server Component

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

The Modal is a Client Component. It receives Cart as children. The Cart component is a Server Component because it is defined without a directive and imported by a server module. The server renders Cart to JSX, serializes that JSX into the RSC Payload, and the client passes it to Modal as a prop. Modal never sees Cart‘s implementation; it only receives the rendered output .

This pattern is the standard way to nest server-rendered content inside client-side wrappers. A modal that toggles visibility with useState can contain a cart that fetches data on the server. The interactive wrapper is a Client Component; the data-fetching content is a Server Component passed as a child.


Complete Example Session

// ============================================
// PART 1: DEFINE A CLIENT COMPONENT
// ============================================
// app/ui/search.tsx
'use client'
import { useState } from 'react'

export default function Search() {
  const [query, setQuery] = useState('')
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />
}

// The directive marks the client entry point.
// The component uses useState and onChange.
// ============================================
// PART 2: IMPORT IT INTO A SERVER COMPONENT
// ============================================
// app/page.tsx (no directive)
import Search from './ui/search'
import Logo from './ui/logo' // Server Component

export default function Page() {
  return (
    <header>
      <Logo />
      <Search />
    </header>
  )
}

// Page and Logo are Server Components.
// Search is a Client Component.
// ============================================
// PART 3: PASS SERIALIZABLE PROPS
// ============================================
// app/ui/chart.tsx
'use client'
export default function Chart({ data }: { data: number[] }) {
  return <div>{/* render chart with data */}</div>
}

// app/page.tsx
<Chart data={[1, 2, 3, 4]} />

// data is a plain array. It serializes.
// ============================================
// PART 4: A FUNCTION PROP FAILS
// ============================================
// app/page.tsx
<Chart onUpdate={() => {}} /> // ❌

// onUpdate is a plain function.
// Functions do not serialize across the boundary.
// React throws an error.
// ============================================
// PART 5: A SERVER FUNCTION PROP WORKS
// ============================================
// app/actions.ts
'use server'
export async function saveData(data: number[]) {
  await db.chart.save({ data })
}

// app/page.tsx
import { saveData } from './actions'
<Chart data={[1, 2, 3]} onSave={saveData} /> // ✅

// saveData is a Server Function.
// It crosses as a reference, not as code.
// ============================================
// PART 6: THE CHILDREN SLOT
// ============================================
// app/ui/card.tsx
'use client'
export default function Card({ children }: { children: React.ReactNode }) {
  return <div className="card">{children}</div>
}

// app/page.tsx
import Card from './ui/card'
import UserList from './ui/user-list' // Server Component

export default function Page() {
  return (
    <Card>
      <UserList />
    </Card>
  )
}

// UserList is rendered on the server.
// Card receives the rendered output as children.
// ============================================
// PART 7: PUSH THE DIRECTIVE DOWN
// ============================================
// Wrong: the whole layout is a Client Component
'use client'
export default function Layout({ children }) { ... } // ❌

// Correct: only the interactive search is client
// Layout is a Server Component
// Search is a Client Component
// ============================================
// PART 8: MULTIPLE CLIENT COMPONENTS
// ============================================
// app/page.tsx (Server Component)
import Header from './ui/header' // Server
import Search from './ui/search' // Client
import Nav from './ui/nav' // Client

export default function Page() {
  return (
    <>
      <Header />
      <Search />
      <Nav />
    </>
  )
}
// ============================================
// PART 9: CLIENT COMPONENT INSIDE CLIENT COMPONENT
// ============================================
// app/ui/search.tsx
'use client'
import Filter from './filter' // no directive needed

export default function Search() {
  return <div><Filter /><input /></div>
}

// Filter is already in the client subtree.
// No directive required.
// ============================================
// PART 10: THE COMPOSITION RULE
// ============================================
// Server → Client: allowed (import and render)
// Client → Server: not allowed (import fails)
// Client receives Server as children: allowed (passed by server)

The ten parts covered the client entry point, importing into a server component, serializable props, the function-prop failure, Server Functions as props, the children slot, pushing the directive down, multiple client components, client-inside-client, and the composition rule.


Quick Reference

Import direction

DirectionAllowedReason
Server imports ClientYesClient Component is designed for both environments
Client imports ServerNoServer code cannot run in the browser
Client receives Server as childrenYesServer renders it first, passes JSX

Serializable props

TypeCrosses
string, number, boolean, null, undefinedYes
plain object, arrayYes
Date, Map, Set, PromiseYes
JSX element (children)Yes
Server Function ("use server")Yes (reference)
plain functionNo
class instanceNo

When to add "use client"

NeedDirective
useState, useReducerYes
useEffect, useLayoutEffectYes
onClick, onChangeYes
window, localStorageYes
createContextYes
Data fetching, DB accessNo (Server Component)
Static contentNo (Server Component)

Composition patterns

PatternResult
Server imports ClientClient Component entry point
Server passes Client as childrenNormal nesting
Server passes Server as children to ClientServer Component renders on server
Client imports ClientNo directive needed

Best Practices

✅ Do This:

// Import Client Components into Server Components
import Search from './ui/search'                           // ✅
// Pass only serializable props
<Chart data={[1, 2, 3]} />                                  // ✅
// Use children to nest Server Components in Client wrappers
<Modal><Cart /></Modal>                                     // ✅
// Push the directive down to the smallest interactive unit
// Only the button file has 'use client'                      // ✅

❌ Don’t Do This:

// Import a Server Component into a Client Component
'use client'
import ServerComp from './server' // ❌ (build error)
// Pass a plain function from Server to Client
<Button onClick={() => {}} /> // ❌
// Add 'use client' to the whole page for one interactive element
'use client'
export default function Page() { ... } // ❌
// Add 'use client' to a Client Component imported by another Client Component
// The directive is redundant                                // ❌

Common Pitfalls

PitfallWhy It HappensFix
Build error “Client Component cannot import Server”Client module imports server modulePass Server Component as children from a Server Component
Runtime error “Functions cannot be passed”Plain function passed as propUse a Server Function or define the handler in the Client Component
Client bundle too large"use client" placed too highMove the directive to the smallest interactive component
"use client" has no effectImported by another client moduleExpected; directive only matters at the server-client entry
Children render failsNon-serializable value passedEnsure children is JSX, not a function

Real-World Examples

1. Server imports Client

import Search from './search'
<Search />

2. Serializable array prop

<Chart data={[1, 2, 3]} />

3. Server Function prop

'use server'
export async function save(data: number[]) { ... }
<Chart onSave={save} />

4. Children slot

<Modal>
  <Cart />
</Modal>

5. Pushing directive down

// page.tsx (Server)
<article>{content}<LikeButton /></article>

6. Client inside client

'use client'
import Filter from './filter' // no directive

7. Multiple client entry points

<Search />
<Nav />

8. Server content in Client wrapper

<ClientWrapper>
  <ServerContent />
</ClientWrapper>

9. Non-serializable error

<Button onClick={() => {}} /> // error

10. Correct handler in client

'use client'
function Button() { const handleClick = () => {}; return <button onClick={handleClick} /> }

Visual

The one-way import rule

┌─────────────────────────────────────────────────────────────┐
│  IMPORT DIRECTION                                            │
│                                                              │
│  Server Component ──imports──► Client Component  ✅          │
│                                                              │
│  Client Component ──imports──► Server Component  ❌          │
│                                                              │
│  Why: Server code cannot run in the browser.                │
│  The bundler rejects the import at build time.              │
└─────────────────────────────────────────────────────────────┘

Children slot flow

┌─────────────────────────────────────────────────────────────┐
│  CHILDREN SLOT                                              │
│                                                              │
│  Server Component (Page)                                    │
│       │                                                      │
│       ├── imports Modal (Client)                             │
│       │                                                      │
│       └── imports Cart (Server)                              │
│             │                                                │
│             ▼                                                │
│  <Modal>                                                    │
│    <Cart />  ← rendered on server, passed as children       │
│  </Modal>                                                    │
│                                                              │
│  Modal receives serialized JSX.                              │
│  Cart's code never enters the client bundle.                │
└─────────────────────────────────────────────────────────────┘

Serializable vs non-serializable

┌─────────────────────────────────────────────────────────────┐
│  PROPS ACROSS THE BOUNDARY                                   │
│                                                              │
│  CROSSES                          DOES NOT CROSS             │
│  ────────                          ─────────────             │
│  string, number, boolean           plain function            │
│  plain object, array               class instance            │
│  Date, Map, Set, Promise           event handler             │
│  JSX element (children)                                      │
│  Server Function (reference)                                 │
└─────────────────────────────────────────────────────────────┘

Push the directive down

┌─────────────────────────────────────────────────────────────┐
│  BUNDLE SIZE                                                 │
│                                                              │
│  WRONG: directive at page level                              │
│    page.tsx ('use client')                                   │
│      → entire page + all imports in client bundle            │
│                                                              │
│  RIGHT: directive at button level                            │
│    page.tsx (Server Component)                               │
│      → page rendered on server                               │
│    like-button.tsx ('use client')                            │
│      → only button code in client bundle                     │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Primary composition directionServer imports Client
Reverse directionClient cannot import Server
Escape hatchServer passes Server Component as children
Prop requirementSerializable values only
Function propsPlain functions fail; Server Functions work
Directive entry pointOnly the file imported by a Server Component
Client-inside-clientNo directive needed
Bundle disciplinePush "use client" down to smallest unit

Key takeaways:

  • Server Components can import and render Client Components. This is the normal composition path. The Client Component’s code ships to the browser, and the server renders it to HTML for the initial page .
  • Client Components cannot import Server Components. The bundler rejects the import because server code cannot run in the browser. This is a hard rule, not a convention .
  • The children slot is the workaround. A Server Component renders the server content and passes it as children to a Client Component. The Client Component receives rendered JSX, not the Server Component’s code .
  • Props must be serializable. Primitives, plain objects, arrays, Date, JSX, and Server Functions cross. Plain functions and class instances do not. The Client Component must define its own event handlers or receive a Server Function reference .
  • Only the entry point needs "use client". A file imported by a Server Component needs the directive. Files imported by that file do not. The directive marks the boundary, not every file in the client subtree .
  • Push the directive down. The client bundle contains everything below the boundary. Place the directive on the smallest interactive component, not the page or layout, to keep the bundle small .

Remember: Composition in RSC is one-way by default. Server Components import Client Components. Client Components cannot import Server Components. The children slot lets a Client Component display server-rendered content without violating that rule. Pass only serializable props, use Server Functions for mutations, and place "use client" as low in the tree as possible. The result is a page where the static content and data fetching run on the server, and only the interactive fragments ship to the browser.

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!