C.W.K.
Stream
Lesson 07 of 08 · published

Composition Patterns

~20 min · composition, providers, children

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Pattern 1: Server wraps Client (children)

Most common shape. A Server Component fetches data and renders a Client Component, passing other Server Components as children. The client wrapper handles interactivity; the children stay server-rendered.

Pattern 2: Context providers

React Context requires a Client Component (it uses hooks). Build a thin 'use client' provider and place it inside the root layout. Everything else can stay server.

Pattern 3: Slots via props

For more deliberate composition (e.g., a modal that needs a header, body, and footer rendered server-side), accept multiple ReactNode props instead of a single children. The Client Component arranges them but doesn't render them.

Code

Pattern 1: server parent + client wrapper + server children·tsx
// app/page.tsx — Server
import { Disclosure } from '@/components/Disclosure';
import { TermsContent } from '@/components/TermsContent';

export default async function Page() {
  const terms = await loadTerms(); // Server fetch
  return (
    <Disclosure label="Read the terms">
      <TermsContent text={terms} />  {/* Server-rendered subtree */}
    </Disclosure>
  );
}

// components/Disclosure.tsx — Client wrapper
'use client';
import { useState } from 'react';
export function Disclosure({ label, children }: { label: string; children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <details open={open} onToggle={(e) => setOpen(e.currentTarget.open)}>
      <summary>{label}</summary>
      {children}
    </details>
  );
}
Pattern 2: thin Client provider in the root layout·tsx
// providers/ThemeProvider.tsx — Client
'use client';
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext<'light' | 'dark'>('dark');
export const useTheme = () => useContext(ThemeContext);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme] = useState<'light' | 'dark'>('dark');
  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

// app/layout.tsx — Server, wraps everything
import { ThemeProvider } from '@/providers/ThemeProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en"><body>
      <ThemeProvider>{children}</ThemeProvider>
    </body></html>
  );
}

External links

Exercise

Build a <TabPanel> as a Client Component that renders tabs but accepts each tab's content as server-rendered children. Demonstrate that your tab body components stay out of the client bundle.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.