Skip to content

Component Composition

Composition is how you combine Server and Client Components without collapsing your entire tree into client-side JavaScript. This lesson covers the key patterns for mixing the two effectively.

The "Wrap, Don't Import" Pattern

A Client Component cannot directly import and render a Server Component's file — but it can accept a pre-rendered Server Component through its children prop (or any other prop). This is the core composition pattern in the App Router: build interactive "shells" as Client Components, and pass server-rendered content into them from a parent Server Component.

This lets you keep something like a modal's open/close logic as a small Client Component, while the actual content inside the modal — which might need to fetch data — stays a Server Component, rendered from above and simply passed down.

// components/Modal.tsx (Client Component — interactive shell)
"use client";
import { useState } from "react";

export default function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      {open && <div className="modal">{children}</div>}
    </>
  );
}

// app/page.tsx (Server Component — provides content)
import Modal from "@/components/Modal";
import ProductDetails from "@/components/ProductDetails"; // a Server Component

export default function Page() {
  return (
    <Modal>
      <ProductDetails productId="42" />
    </Modal>
  );
}

Modal never imports ProductDetails directly — it receives the already-rendered result as children, so ProductDetails (and any data fetching it does) never becomes client JavaScript.

Composition Rules to Remember

Server Component  → can import  → Server or Client Components
Client Component  → can import  → only Client Components
Client Component  → can receive → Server Components as children/props (pre-rendered)
  • Server Components can freely import and render both Server and Client Components.
  • Client Components can only directly import other Client Components.
  • A Server Component's output can still reach a Client Component if it's passed in as children or another prop.
  • This pattern is sometimes described as "lifting state up, pushing content down."

Composition Patterns Cheat Sheet

Common shapes for combining Server and Client Components.

Pattern Use Case
Client shell + Server children Modals, tabs, accordions with server-rendered content
Server page + Client leaf component A page with one small interactive widget
Client Component + Client children only Fully interactive widgets (date pickers, forms)
Server Component fetching, Client Component displaying via props Charts, interactive tables

Interleaving Server and Client Components

A realistic page often interleaves both types several times: a Server Component page renders a Client Component <Tabs> shell, which receives several Server Component panels as children, one of which contains a small Client Component <LikeButton> for interactivity.

  • app/page.tsx (Server) renders <Tabs> (Client), passing panels as children.
  • Each panel (Server) renders article content plus a <LikeButton> (Client) for interactivity.
  • Only <Tabs> and <LikeButton> end up in the client JavaScript bundle.

Context Providers Must Be Client Components

React Context relies on client-side subscription behavior, so any component calling createContext/useContext's provider must be a Client Component. The common pattern is a small Providers Client Component near the root layout that wraps children (which can still include Server Components beneath it).

// app/providers.tsx
"use client";
import { ThemeProvider } from "./theme-context";

export default function Providers({ children }: { children: React.ReactNode }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}

Common Mistakes

  • Trying to import a Server Component's file directly inside a Client Component instead of passing it as a prop.
  • Wrapping the entire app in a Client Component Providers and assuming everything inside automatically becomes client-rendered too.
  • Forgetting Context providers must be Client Components, then being confused by a build error.
  • Over-lifting state to the top of the tree instead of composing smaller, focused Client Components.

Key Takeaways

  • Client Components can't directly import Server Components, but can receive them as children/props.
  • Server Components can import both Server and Client Components freely.
  • Common composition pattern: interactive Client "shells" wrapping server-rendered content.
  • Context providers must be Client Components, typically isolated near the root layout.
  • Good composition keeps most of the tree server-rendered while isolating interactivity precisely.

Pro Tip

When a design calls for something interactive wrapping a lot of content (a modal, an accordion, a carousel), build the interactive behavior as a minimal, content-agnostic Client Component that just renders children — this keeps the actual content fully flexible and server-renderable.