Composition Patterns
Practical patterns for mixing Server and Client Components without accidentally bloating your client bundle.
Push Client Boundaries Down
The single most valuable composition habit: keep "use client" as low (as deep) in the component tree as possible. Instead of marking an entire page as a Client Component for one interactive button, extract just that button into its own small Client Component and leave everything else as Server Components.
Client Boundary Too High
'use client'; // now the ENTIRE page ships to the browser
export default function ProductPage({ product }) { const [qty, setQty] = useState(1); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <input value={qty} onChange={e => setQty(e.target.value)} /> </div> );}Client Boundary Pushed Down
// page.tsx — stays a Server Componentexport default function ProductPage({ product }) { return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <QuantityPicker /> </div> );}
// QuantityPicker.tsx'use client';export default function QuantityPicker() { const [qty, setQty] = useState(1); return <input value={qty} onChange={e => setQty(e.target.value)} />;}The Children-Slot Pattern
As covered in the previous lesson, a Client Component can accept Server Components through its children (or any prop), as long as the parent Server Component is the one that renders them. This lets a Client Component provide interactive "chrome" — a modal, an accordion, a tab container — around content that stays server-rendered.
// Accordion.tsx — Client Component, purely structural/interactive'use client';export default function Accordion({ title, children }) { const [open, setOpen] = useState(false); return ( <div> <button onClick={() => setOpen(!open)}>{title}</button> {open && <div>{children}</div>} </div> );}// page.tsx — Server Componentimport Accordion from './Accordion';import ServerRenderedArticle from './ServerRenderedArticle';
export default function Page() { return ( <Accordion title="Read more"> <ServerRenderedArticle /> </Accordion> );}Sharing Data Without Prop Drilling
For data needed deep in a Client Component tree, prefer fetching it once in a Server Component near the top and passing it down, rather than fetching independently in many nested Client Components. If several unrelated Client Components need the same piece of state, a Context provider (itself a small Client Component) is the standard React answer.
A Complete Example
A typical, well-composed page: a Server Component page fetches data, renders mostly static Server Components, and drops in a handful of small Client Components exactly where interactivity is required.
// app/blog/[slug]/page.tsx — Server Componentimport LikeButton from './LikeButton'; // Client Component
export default async function PostPage({ params }) { const post = await getPost(params.slug); // runs on the server
return ( <article> <h1>{post.title}</h1> <p>{post.body}</p> <LikeButton postId={post.id} initialLikes={post.likes} /> </article> );}Common Beginner Mistakes
This makes every page under that layout ship extra JavaScript — instead, extract just the interactive nav toggle or theme switch into its own small Client Component.
Fetch it once in a Server Component higher up and pass it down as props, or lift it into shared client state if it needs to change after the initial load.
FAQs
As small as the actual interactive behavior requires — a single button, input, or toggle is a completely normal size for a Client Component.
Yes — most component libraries that use hooks or state need "use client" wherever they're first imported, so wrapping them in a small local Client Component is a common technique.
Summary
Good composition means pushing "use client" as deep as possible and using the children-slot pattern to keep Server Components server-rendered even inside interactive UI. Next, you'll zoom out to the rendering strategies these components participate in — SSR, SSG, ISR, and CSR.