Post 02
Server vs Client Components, Kept Real
The Server/Client split confused almost everyone at some point — not because it is hard, but because people explain it like a compiler paper. Here is the plain-language version, plus the composition patterns that keep the boundary small.

The Server Components model is genuinely a good idea, and the way it usually gets taught makes it sound far more complicated than it is. Here is the version I give people on their first day.
Backstage crew and performers
Think of your app as a theatre production.
- Server Components are the backstage crew. They build the set, place the props, prepare the lighting — before the curtain goes up, and the audience never sees them. They run on the server, can read a database or a filesystem, and send finished HTML.
- Client Components are the performers. They are on stage, reacting to the audience in real time. They run in the browser, use state and effects, and respond to clicks and typing.
The rule
Start every component on the server. Move the boundary to the client only at the point where you need interaction — state, effects, event handlers, browser APIs.
In the App Router, components are Server Components by default. You opt into the client with "use client" at the top of a file, and that marks that file and everything it imports as client code.
Where the boundary goes
The skill is putting "use client" as far down the tree as possible. A page that is mostly static with one interactive widget should be a Server Component that renders a small Client Component, not the reverse.
1export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {2 const { id } = await params;3 const product = await getProduct(id);4 5 return (6 <article>7 <ProductGallery images={product.images} /> {/* server */}8 <h1>{product.name}</h1> {/* server */}9 <RichText value={product.description} /> {/* server */}10 <AddToCartButton productId={product.id} /> {/* client — just this */}11 </article>12 );13}Only AddToCartButton needs "use client". The gallery, the copy, the layout — all rendered on the server, none of it shipped as JavaScript. That is the whole payoff: less code in the browser.
Server Components render Client Components, not the reverse
A Server Component can import and render a Client Component directly. A Client Component cannot import a Server Component — but it can accept one as children or as a prop. That pattern lets you keep a server-rendered subtree inside a client-rendered shell.
1"use client";2import { useState } from "react";3 4export function Collapsible({ children }: { children: React.ReactNode }) {5 const [open, setOpen] = useState(false);6 return (7 <div>8 <button aria-expanded={open} onClick={() => setOpen(!open)}>9 {open ? "Hide" : "Show"} details10 </button>11 {open && children} {/* this can be a Server Component */}12 </div>13 );14}1// app/page.tsx (Server Component)2import { Collapsible } from "@/components/collapsible";3import { HeavyServerTable } from "@/components/heavy-server-table";4 5export default function Page() {6 return (7 <Collapsible>8 {/* rendered on the server, streamed as HTML into the client shell */}9 <HeavyServerTable />10 </Collapsible>11 );12}The context provider pattern
Context needs a Client Component, but you want to wrap your whole server-rendered tree in it. The move: a thin client provider in layout.tsx, with {children} (server) passed through.
1"use client";2import { ThemeProvider } from "next-themes";3import { QueryClientProvider, QueryClient } from "@tanstack/react-query";4import { useState } from "react";5 6export function Providers({ children }: { children: React.ReactNode }) {7 const [qc] = useState(() => new QueryClient());8 return (9 <QueryClientProvider client={qc}>10 <ThemeProvider attribute="class">{children}</ThemeProvider>11 </QueryClientProvider>12 );13}14// children stays a Server Component tree — only the providers are clientThe cheat sheet
| Situation | Server | Client |
|---|---|---|
| Fetching data for the page | Yes | No |
| Reading secrets / env / the database | Yes | Never |
onClick, onChange, form interactivity | No | Yes |
useState, useEffect, useRef, useContext | No | Yes |
window, localStorage, browser APIs | No | Yes |
| Third-party components that use hooks | No | Yes |
Do not overthink it. If it can run before the user shows up, it runs on the server. If it needs the user, it runs on the client. Everything else is keeping that boundary as small as you can.
Found this useful? Pass it on.
Want a custom write-up for your team? Get in touch.
Building something like this?
If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.