---
title: "17 — Rendering Composition — Server and Client Components in One Tree"
uid: rendering-composition
tags: ["roadmap:nextjs", "client-components", "server-components", "rendering", "composition", "nextjs"]
excerpt: "Server Components render on the server by default (zero JS shipped); Client Components are the opt-in for interactivity. Composition is placing the client boundary as low and small as possible."
date: 2026-08-13T03:27:59+0000
source: https://www.aveshina.my.id/en/blog/rendering-composition
---

Mixing two kinds of components in one tree sounded chaotic until the default made the rule obvious. The model that clicked: **Server Components are the default (render on the server, ship zero JS); Client Components are the deliberate opt-in for interactivity.** [1][2] Composition is the art of placing the client boundary as low and small as possible, because everything inside it ships JavaScript.

## The two component kinds

**Server Components** render on the server. They can fetch data directly with async/await, access backend resources, and ship zero JavaScript to the browser — their output is HTML, not a component that re-hydrates (gets its JavaScript wired back up on the client) [2][3]. They're the default in the App Router.

**Client Components** are opted into with the 'use client' directive. They render on the client (after an initial server render), can use useState, useEffect, event handlers, and browser APIs. They're the right home for interactivity — buttons, forms, animations, anything that responds to the user [3][4].

```
// Server Component (default) — data fetched on the server, zero JS shipped
export default async function Page() {
  const posts = await fetchPosts();
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
      <LikeButton postId={posts[0].id} /> {/* client island */}
    </ul>
  );
}
```

```
// app/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!p)}>{liked ? '♥' : '♡'}</button>;
}
```

## The composition rules

The rules that govern how these compose are what make the model tractable [1][2]:

- **Server → Client:** A Server Component can import and render a Client Component. This is the normal case — the page (server) renders an interactive widget (client) and passes it props.
- **Client → Server:** A Client Component **cannot** import a Server Component. Once you're in the client boundary, everything imported into it becomes client too.
- **Passing Server Components into Client Components:** The escape hatch is the children prop. A Client Component can accept a Server Component as a prop (typically children) without importing it — the Server Component is rendered on the server and handed in as already-rendered output.

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Composition rules. A Server Component (indigo) imports and renders a Client Component (amber) — allowed, props passed down. A Client Component cannot import a Server Component — but can accept one as a children prop, rendered on the server and handed in.">
  <defs>
    <marker id="carrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Rule 1: Server -> Client -->
    <text x="360" y="24" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">✓ Server imports Client — props flow down</text>
    <rect x="60" y="40" width="160" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="140" y="62" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Server</text>
    <text x="140" y="78" font-size="9" fill="#475569" text-anchor="middle">data fetched, zero JS</text>
    <line x1="220" y1="65" x2="320" y2="65" stroke="#16a34a" stroke-width="1.5" marker-end="url(#carrow)"/>
    <text x="270" y="58" font-size="9" fill="#052e16" text-anchor="middle">renders</text>
    <rect x="320" y="40" width="160" height="50" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="400" y="62" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">Client</text>
    <text x="400" y="78" font-size="9" fill="#475569" text-anchor="middle">useState, onClick</text>

    <!-- Rule 2: Client -> Server (forbidden) -->
    <text x="360" y="130" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">✗ Client cannot import Server</text>
    <rect x="60" y="146" width="160" height="50" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="140" y="168" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">Client</text>
    <line x1="220" y1="171" x2="320" y2="171" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3"/>
    <text x="270" y="164" font-size="9" fill="#7f1d1d" text-anchor="middle">cannot import</text>
    <rect x="320" y="146" width="160" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5" opacity="0.5"/>
    <text x="400" y="168" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Server</text>

    <!-- Rule 3: children escape hatch -->
    <text x="360" y="232" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">✓ …but Client can take a Server as `children` prop</text>
    <rect x="120" y="248" width="180" height="22" rx="6" fill="#dcfce7" stroke="#16a34a"/>
    <text x="210" y="263" font-size="9" fill="#052e16" text-anchor="middle">Server rendered, handed in</text>
  </g>
</svg>
```

## Why the boundary placement matters

Every component imported into a Client Component also becomes a Client Component — the boundary is contagious downward. So the goal is to push the 'use client' directive as far down the tree as possible, isolating interactivity in small "client islands" while keeping the surrounding shell on the server [1]. A whole-page 'use client' ships the whole page's JS; a small interactive widget marked 'use client' ships only that widget's JS.

This is the strategic decision: which parts of the UI genuinely need to be interactive (state, effects, event handlers), and which can stay as server-rendered HTML. The art is in finding the smallest client islands that still deliver the interactivity.

## How I use this

Every page starts as a Server Component. I add Client Components only when a piece genuinely needs interactivity or browser APIs, and I keep them small — a button, a form, a widget — rather than wrapping whole sections. When a Client Component needs server-rendered content around it, I pass that content in as children rather than importing a Server Component. The discipline pays off in bundle size: the more that stays on the server, the less JavaScript ships to the browser.

## References

[1] Vercel, "Server and Client composition patterns," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/14/app/building-your-application/rendering/composition-patterns](https://nextjs.org/docs/14/app/building-your-application/rendering/composition-patterns)

[2] Vercel, "Server and Client Components," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/getting-started/server-and-client-components#how-do-server-and-client-components-work-in-nextjs](https://nextjs.org/docs/app/getting-started/server-and-client-components#how-do-server-and-client-components-work-in-nextjs)

[3] Vercel, "Server and Client Components — when to use," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/getting-started/server-and-client-components#when-to-use-server-and-client-components](https://nextjs.org/docs/app/getting-started/server-and-client-components#when-to-use-server-and-client-components)

[4] React, "Server Components," React Docs, 2024. [Online]. Available: [https://react.dev/reference/rsc/server-components](https://react.dev/reference/rsc/server-components)

[5] "Next.js 15 tutorial — Server and Client Components," YouTube, 2024. [Video]. Available: [https://www.youtube.com/watch?v=dMCSiA5gzkU](https://www.youtube.com/watch?v=dMCSiA5gzkU)

```quiz
Q: In the App Router, components are Server Components by default. How do you opt into client behavior?
- Wrap the component in <Client>
- Add the 'use client' directive at the top of the file
correct: 1
explain: The 'use client' directive marks a file (and everything it imports) as a Client Component, opting into useState, useEffect, event handlers, and browser APIs.

Q: A Client Component can directly import a Server Component. True or false?
- True
- False — once you're in the client boundary, everything imported becomes client too
correct: 1
explain: Client Components cannot import Server Components. The boundary is contagious downward. The escape hatch is passing server-rendered content as the children prop.

Q: Why push the 'use client' directive as far down the tree as possible?
- To improve type safety
- To minimize the JavaScript shipped — everything inside the client boundary becomes client code
correct: 1
explain: Smaller client islands ship less JS. A whole-page 'use client' bundles the whole page; a small widget directive bundles only that widget.

Q: How can a Client Component display server-rendered content without importing a Server Component?
- It can't — all content in a Client Component is client-rendered
- Accept the Server Component's output as a `children` prop, rendered on the server and handed in
correct: 1
explain: The children prop is the escape hatch: the parent Client Component receives already-rendered server output, avoiding a direct import.

Q: Which is the right home for an interactive like button with onClick and useState?
- Server Component
- Client Component
correct: 1
explain: Interactivity (event handlers, state, effects) requires the client. The like button is a small Client Component island inside a Server Component page.
```
