You know when to use Server and Client Components. Now let's master how to compose them together. Component composition is the art of combining different component types to build sophisticated applications. The right patterns let you keep most code as Server Components (performance!) while strategically adding Client Components for interactivity. These patterns are the secret to building Next.js apps that are both fast and interactive.
The Composition Problem
We have a challenge:
❌ This Doesn't Work
'use client';
// Client Component trying to import Server Component
import { ServerComponent } from './ServerComponent';
export function ClientComponent() {
return <ServerComponent />; // ❌ Error!
}Client Components cannot import Server Components directly.
✅ But This Works
// Server Component
export default function Page() {
return (
<ClientComponent>
<ServerComponent /> {/* ✅ Passed as children */}
</ClientComponent>
);
}Server Components can pass Server Components to Client Components via props!
The Key Insight
Client Components can't import Server Components, but they can receive them as props. This is the foundation of all composition patterns.
Pattern 1: Children Prop Pattern
The most fundamental pattern: pass Server Components to Client Components via the children prop.
The Pattern
// Server Component (page)
async function getData() {
const res = await fetch('https://api.example.com/data');
return res.json();
}
export default async function Page() {
const data = await getData();
return (
<ClientWrapper>
{/* Server Component passed as children */}
<ServerContent data={data} />
</ClientWrapper>
);
}
// Server Component
function ServerContent({ data }) {
return (
<div className="prose">
<h2>{data.title}</h2>
<p>{data.description}</p>
</div>
);
}'use client';
import { useState } from 'react';
export function ClientWrapper({
children
}: {
children: React.ReactNode
}) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="border rounded-lg p-4">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="mb-4 px-4 py-2 bg-blue-600 text-white rounded"
>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
{/* Server Component rendered here */}
{isExpanded && (
<div className="mt-4">
{children}
</div>
)}
</div>
);
}
// ✅ Client Component adds interactivity
// ✅ Server Component handles data and rendering
// ✅ Clean separation of concernsWhen to Use
- Client Component needs to wrap/control Server Component
- Adding interactive wrapper (collapsible, modal, etc.)
- Want to keep content as Server Component
Real-World Example: Collapsible Section
import { Collapsible } from '@/components/Collapsible';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
}
async function getComments(postId: string) {
const res = await fetch(`https://api.example.com/posts/${postId}/comments`);
return res.json();
}
export default async function BlogPostPage({ params }) {
const post = await getPost(params.slug);
const comments = await getComments(post.id);
return (
<article>
{/* Server Component: Post content */}
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client Component wrapper with Server Component children */}
<Collapsible title="Comments">
{/* Server Component: Comments */}
<CommentList comments={comments} />
</Collapsible>
</article>
);
}
// Server Component
function CommentList({ comments }) {
return (
<div className="space-y-4">
{comments.map(comment => (
<div key={comment.id} className="border-l-4 border-gray-300 pl-4">
<p className="font-semibold">{comment.author}</p>
<p className="text-gray-700">{comment.content}</p>
</div>
))}
</div>
);
}'use client';
import { useState } from 'react';
export function Collapsible({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="mt-8 border rounded-lg">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full px-6 py-4 flex items-center justify-between bg-gray-50 hover:bg-gray-100"
>
<span className="text-lg font-semibold">{title}</span>
<span className="text-2xl">{isOpen ? '−' : '+'}</span>
</button>
{isOpen && (
<div className="p-6">
{children}
</div>
)}
</div>
);
}Pattern 2: Slot Pattern
Pass multiple Server Components to specific "slots" in a Client Component using named props.
The Pattern
// Server Component (page)
async function getStats() {
return await db.query('SELECT * FROM stats');
}
async function getRecentActivity() {
return await db.query('SELECT * FROM activity ORDER BY date DESC LIMIT 10');
}
export default async function DashboardPage() {
const stats = await getStats();
const activity = await getRecentActivity();
return (
<DashboardLayout
header={<Header stats={stats} />}
sidebar={<Sidebar />}
main={<MainContent stats={stats} />}
footer={<RecentActivity activity={activity} />}
/>
);
}
// Server Components
function Header({ stats }) {
return (
<div className="flex justify-between items-center">
<h1>Dashboard</h1>
<p>Total Revenue: ${stats.revenue}</p>
</div>
);
}
function Sidebar() {
return (
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/analytics">Analytics</a>
<a href="/dashboard/settings">Settings</a>
</nav>
);
}
function MainContent({ stats }) {
return (
<div className="grid grid-cols-3 gap-6">
<StatCard title="Users" value={stats.users} />
<StatCard title="Orders" value={stats.orders} />
<StatCard title="Revenue" value={`$${stats.revenue}`} />
</div>
);
}
function RecentActivity({ activity }) {
return (
<div>
<h3>Recent Activity</h3>
<ul>
{activity.map(item => (
<li key={item.id}>{item.description}</li>
))}
</ul>
</div>
);
}'use client';
import { useState } from 'react';
interface DashboardLayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
main: React.ReactNode;
footer: React.ReactNode;
}
export function DashboardLayout({
header,
sidebar,
main,
footer,
}: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="min-h-screen bg-gray-50">
{/* Header slot */}
<header className="bg-white shadow px-6 py-4">
{header}
</header>
<div className="flex">
{/* Sidebar slot */}
<aside
className={`bg-white shadow transition-all ${
sidebarOpen ? 'w-64' : 'w-0 overflow-hidden'
}`}
>
<div className="p-6">
{sidebar}
</div>
</aside>
{/* Main content slot */}
<main className="flex-1 p-6">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="mb-4 px-4 py-2 bg-blue-600 text-white rounded"
>
{sidebarOpen ? 'Hide' : 'Show'} Sidebar
</button>
{main}
</main>
</div>
{/* Footer slot */}
<footer className="bg-white shadow px-6 py-4">
{footer}
</footer>
</div>
);
}
// ✅ Client Component controls layout
// ✅ Server Components fill the slots
// ✅ Clean, flexible architectureWhen to Use
- Complex layouts with multiple sections
- Need to position Server Components in specific places
- Each section has different data requirements
Pattern 3: Wrapper Pattern
Wrap multiple Server Components with a single Client Component that adds shared interactivity.
The Pattern
import { FilterableGrid } from '@/components/FilterableGrid';
async function getProducts() {
return await db.products.findMany();
}
async function getCategories() {
return await db.categories.findMany();
}
export default async function ProductsPage() {
const products = await getProducts();
const categories = await getCategories();
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Products</h1>
{/* Client Component wrapper */}
<FilterableGrid categories={categories}>
{/* Server Components for each product */}
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</FilterableGrid>
</div>
);
}
// Server Component
function ProductCard({ product }) {
return (
<div className="bg-white rounded-lg shadow p-4">
<img
src={product.image}
alt={product.title}
className="w-full h-48 object-cover rounded"
/>
<h3 className="mt-4 font-semibold">{product.title}</h3>
<p className="text-2xl text-green-600">${product.price}</p>
<p className="text-sm text-gray-600">{product.category}</p>
</div>
);
}'use client';
import { useState } from 'react';
export function FilterableGrid({
categories,
children,
}: {
categories: Array<{ id: string; name: string }>;
children: React.ReactNode;
}) {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
return (
<div>
{/* Filter controls */}
<div className="mb-6 flex gap-2">
<button
onClick={() => setSelectedCategory(null)}
className={`px-4 py-2 rounded ${
selectedCategory === null
? 'bg-blue-600 text-white'
: 'bg-gray-200'
}`}
>
All
</button>
{categories.map(category => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`px-4 py-2 rounded ${
selectedCategory === category.id
? 'bg-blue-600 text-white'
: 'bg-gray-200'
}`}
>
{category.name}
</button>
))}
</div>
{/* Grid of Server Components */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{children}
</div>
</div>
);
}When to Use
- Multiple Server Components need shared interactive state
- Filtering, sorting, or searching content
- Layout that responds to user interaction
Pattern 4: Provider Pattern
Use Client Component providers at the root to share state across Server Components.
The Pattern
// Root layout (Server Component)
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{/* Client Component providers wrap Server Components */}
<Providers>
{children}
</Providers>
</body>
</html>
);
}'use client';
import { createContext, useContext, useState } from 'react';
// Create contexts
const ThemeContext = createContext<{
theme: 'light' | 'dark';
toggleTheme: () => void;
} | null>(null);
const CartContext = createContext<{
items: any[];
addItem: (item: any) => void;
removeItem: (id: string) => void;
} | null>(null);
export function Providers({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [cartItems, setCartItems] = useState<any[]>([]);
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
const addItem = (item: any) => {
setCartItems([...cartItems, item]);
};
const removeItem = (id: string) => {
setCartItems(cartItems.filter(item => item.id !== id));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<CartContext.Provider value={{ items: cartItems, addItem, removeItem }}>
<div className={theme === 'dark' ? 'dark' : ''}>
{children}
</div>
</CartContext.Provider>
</ThemeContext.Provider>
);
}
// Export hooks for use in Client Components
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within Providers');
return context;
}
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within Providers');
return context;
}'use client';
import { useTheme } from '@/app/providers';
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="px-4 py-2 rounded bg-gray-200 dark:bg-gray-700"
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}When to Use
- Global state needed across the app (theme, auth, cart)
- Multiple Client Components need to share state
- Context/providers are required
Provider Performance Note
Providers turn their children into Client Components during hydration. Keep providers minimal and only use for truly global state.
Pattern 5: Interleaving Pattern
Mix Server and Client Components in a "sandwich" pattern.
// Server Component (page)
async function getArticle() {
return await db.articles.findFirst();
}
async function getRelatedArticles() {
return await db.articles.findMany({ take: 3 });
}
export default async function ArticlePage() {
const article = await getArticle();
const related = await getRelatedArticles();
return (
<div>
{/* Server Component: Static header */}
<ArticleHeader article={article} />
{/* Client Component: Interactive TOC */}
<TableOfContents sections={article.sections} />
{/* Server Component: Article content */}
<ArticleContent content={article.content} />
{/* Client Component: Share buttons */}
<ShareButtons title={article.title} />
{/* Server Component: Related articles */}
<RelatedArticles articles={related} />
{/* Client Component: Comment form */}
<CommentForm articleId={article.id} />
</div>
);
}
// Mix of Server and Client Components
// Each at the right level for its needsWhen to Use
- Page has both static and interactive sections
- Want to minimize Client Component bundle
- Clear separation between data display and interaction
Advanced Composition Techniques
Technique 1: Render Props
Pass functions that return components:
// Server Component
export default async function Page() {
const data = await getData();
return (
<ClientComponent
renderHeader={() => <ServerHeader data={data} />}
renderContent={() => <ServerContent data={data} />}
/>
);
}
// Client Component
'use client';
export function ClientComponent({ renderHeader, renderContent }) {
const [view, setView] = useState('grid');
return (
<div>
{renderHeader()}
<button onClick={() => setView(view === 'grid' ? 'list' : 'grid')}>
Toggle View
</button>
<div className={view === 'grid' ? 'grid' : 'list'}>
{renderContent()}
</div>
</div>
);
}Technique 2: Component Factory
Server Component decides which Client Component to use:
// Server Component
async function Page({ params }) {
const user = await getUser(params.id);
// Choose Client Component based on data
const Component = user.isPremium
? PremiumDashboard
: FreeDashboard;
return <Component user={user} />;
}Technique 3: Nested Composition
Multiple levels of composition:
// Server Component
export default async function Page() {
return (
<ClientLayout>
<ServerSidebar>
<ClientNavigation>
<ServerNavItems />
</ClientNavigation>
</ServerSidebar>
<ServerContent>
<ClientInteractive>
<ServerData />
</ClientInteractive>
</ServerContent>
</ClientLayout>
);
}
// Multiple levels of Server/Client compositionComposition Best Practices
1. Keep Client Component Boundaries Small
// ❌ BAD: Large Client Component boundary
'use client';
export function Page() {
return (
<div>
<Header /> {/* All become Client Components */}
<Navigation /> {/* All become Client Components */}
<Content /> {/* All become Client Components */}
<Footer /> {/* All become Client Components */}
<InteractiveButton /> {/* Only this needs to be Client */}
</div>
);
}
// ✅ GOOD: Small Client Component boundary
export function Page() {
return (
<div>
<Header /> {/* Server Component */}
<Navigation /> {/* Server Component */}
<Content /> {/* Server Component */}
<Footer /> {/* Server Component */}
<InteractiveButton /> {/* Client Component */}
</div>
);
}2. Use TypeScript for Safety
// Define prop types clearly
interface WrapperProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}
export function Wrapper({ header, sidebar, children }: WrapperProps) {
// TypeScript ensures correct usage
}3. Document Composition Patterns
/**
* Collapsible wrapper component
*
* Usage:
* <Collapsible title="Comments">
* <ServerComponent /> {/* Pass Server Components as children */}
* </Collapsible>
*
* The children can be Server Components - they're passed as props,
* not imported directly.
*/
'use client';
export function Collapsible({ title, children }) {
// ...
}4. Avoid Prop Drilling
// ❌ BAD: Prop drilling through many levels
<ClientWrapper data={data}>
<MiddleComponent data={data}>
<DeepComponent data={data}>
<ActualComponent data={data} />
</DeepComponent>
</MiddleComponent>
</ClientWrapper>
// ✅ GOOD: Use Context or pass directly where needed
<Providers initialData={data}>
<ClientWrapper>
<MiddleComponent>
<DeepComponent>
<ActualComponent /> {/* Gets data from context */}
</DeepComponent>
</MiddleComponent>
</ClientWrapper>
</Providers>Complete Practical Example
Accordion Component (Client Wrapper Pattern)
A Client Component that can wrap Server Component children
Output Preview
Common Composition Mistakes
Mistake 1: Importing Server Component in Client
'use client';
import { ServerComponent } from './ServerComponent'; // ❌
export function ClientComponent() {
return <ServerComponent />; // Won't work!
}
// ✅ Fix: Use children prop
export function ClientComponent({ children }) {
return <div>{children}</div>;
}
// Then in Server Component:
<ClientComponent>
<ServerComponent />
</ClientComponent>Mistake 2: Not Marking Client Boundaries
// ❌ Forgot 'use client'
import { useState } from 'react';
export function Interactive() {
const [count, setCount] = useState(0); // Error!
// ...
}
// ✅ Add 'use client'
'use client';
import { useState } from 'react';
export function Interactive() {
const [count, setCount] = useState(0); // Works!
// ...
}Mistake 3: Passing Non-Serializable Props
// ❌ Can't pass functions from Server to Client
function ServerComponent() {
const handleClick = () => console.log('clicked');
return <ClientComponent onClick={handleClick} />; // Error!
}
// ✅ Define handlers in Client Component
'use client';
function ClientComponent() {
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button>;
}Composition Pattern Examples
Different patterns for composing Server and Client Components
Select a file or folder to see details
Key Takeaways
- Children prop pattern - pass Server Components as children
- Slot pattern - multiple named props for complex layouts
- Wrapper pattern - shared interactivity around Server Components
- Provider pattern - global state at root
- Interleaving pattern - mix Server and Client strategically
- Keep boundaries small - minimize Client Component scope
- Use TypeScript - type safety in composition
- Document patterns - make intentions clear
What's Next?
You've mastered component composition—the key to building sophisticated Next.js applications! Next, we'll explore Passing Props Between Server and Client Components, diving deep into serialization, what data can cross the boundary, and best practices for data flow.
Understanding prop passing is crucial because Server and Client Components live in different environments. You'll learn what works, what doesn't, and how to structure your data for optimal performance.
🎨 Composition is an Art
Component composition is as much art as science. There's often more than one valid way to compose components. Choose patterns that make your code clear, maintainable, and performant. With practice, you'll develop intuition for which pattern fits each situation!