Most Server Components confusion is really one question wearing a disguise: where does this line of code actually run?
TL;DR
- Server Components run once, on the server, and send HTML/serialized output — no JavaScript ships for them.
- Client Components run in the browser and own interactivity, state, and effects.
'use client'marks the boundary; everything imported below it becomes client code.- Pass data down, push interactivity to the leaves, and the architecture mostly designs itself.
The core idea
Think of your component tree as having a waterline. Above it, on the server, components fetch data, read the filesystem, and talk to databases directly — then disappear, leaving only their output. Below the waterline, in the browser, components handle clicks, hold state, and run effects.
The 'use client' directive is where you draw that waterline. It does not mean "this component is interactive" so much as "from here down, we are in the browser."
// Server Component (default) — runs on the server, ships no JS
async function ProductPage({ id }: { id: string }) {
const product = await db.product.find(id) // direct data access, no API route
return <ProductView product={product} />
}
'use client'
// Client Component — runs in the browser, owns interactivity
function AddToCartButton({ id }: { id: string }) {
const [pending, setPending] = useState(false)
return <button onClick={() => addToCart(id)}>Add to cart</button>
}
The rule that resolves most confusion
Server Components can render Client Components, but not the other way around — a client component can only receive server-rendered content as children or props. So the natural shape is: do the data work high up on the server, and pass it down to small interactive islands at the leaves.
Don't ask "should this be a client component?" Ask "does this specific node need the browser?" If not, leave it on the server.
Common traps
- Marking a whole page
'use client'to fix one interactive widget. You just shipped the entire subtree to the browser. Push the directive down to the actual interactive leaf instead. - Passing non-serializable props (functions, class instances) across the boundary. Only serializable data crosses.
- Reaching for
useEffectto fetch data that a Server Component could have loaded directly. If it runs before interactivity, it probably belongs on the server.
Why it is worth the mental tax
Once the waterline clicks, the payoff is real: less JavaScript shipped, data fetching that lives next to the component that needs it, and no API layer invented purely to feed your own frontend. The model is different from the SPA habits many of us built over a decade — but it is simpler underneath, not more complex.
Draw the waterline deliberately, keep interactivity at the edges, and the rest falls into place.