Course outline · 0% complete

0/29 lessons0%

Course overview →

Composing the two worlds

lesson 3-3 · ~11 min · 9/29

Mixing without dragging

Rules of composition:

  1. A server component can render a client component. This is the normal direction: the page (server) sprinkles in interactive leaves (client).
  2. A client component cannot import a server component. Importing pulls it into the client bundle, where server code cannot run.
  3. But a client component can receive server-rendered content as children. The classic props pattern from the React course is the escape hatch.

Pattern 3 is worth a picture in code:

// app/page.js  (server)
import Collapsible from "./collapsible"; // a "use client" wrapper
import HeavyArticle from "./article";    // stays a server component

export default function Page() {
  return (
    <Collapsible>
      <HeavyArticle />
    </Collapsible>
  );
}

Collapsible handles the open/close click in the browser, while HeavyArticle is rendered on the server and passed in as already-finished content.

Why Collapsible keeps HeavyArticle off the wire

The reason the pattern works is the order of operations. The server renders HeavyArticle first, then passes the finished result into the client component as children.

From Collapsible's point of view there is no article component at all, only a slot of already-rendered content that arrived as a prop. It never imports the article's code, so that code is never added to the client bundle and never travels to the browser. Collapsible ships only the few lines it needs to toggle a boolean and show or hide its slot.

This is the same "components as props" composition you used in the React course. Here it does double duty, keeping bundle size down as well as keeping code tidy.

What an import across the boundary costs

Now imagine a "use client" component adding import HeavyArticle from "./article" at the top of its file, where article.js has no directive of its own. HeavyArticle is pulled across the boundary and becomes client code, shipped to the browser.

The boundary is drawn by imports, not by each file's own directive. Importing a file from client code drags that file into the client bundle regardless of what the file itself says. Worse, if the imported file uses server-only APIs such as a database client or a secret environment variable, the build errors out, because those things genuinely cannot run in a browser.

That is exactly why rule 2 says a client component cannot import a server component. When you need server-rendered content inside a client component, pass it as children instead.

Choosing in practice

A quick decision table you can apply to any component:

What the component doesWhere it belongs
Uses useState, useEffect, refs, or event handlersclient
Uses browser APIs such as localStorage or windowclient
Fetches data, reads env vars, or queries a databaseserver (the default, do nothing)
Just renders markup from propsserver
Both data and interactivitysplit it in two

That last row is the common case in real code. Write a server component for the data and markup, then give it a small client child for the interactive part.

This mirrors the frontend/backend split you already lived in the React and Backend with Node.js courses, except the boundary now runs through one component tree instead of between two repos.

Counting client components in a blog page

Here is a realistic tree, with four components:

ComponentWhat it doesVerdict
PostBodyRenders markdown from propsserver
CommentFormTextarea driven by useStateclient
AuthorCardRenders propsserver
ThemeToggleonClick plus localStorageclient

Two of the four need "use client": CommentForm, because it holds state, and ThemeToggle, because it handles a click and reaches for a browser API.

PostBody and AuthorCard only turn props into markup, so they stay server components and ship no JavaScript at all.

Why it works out that way

  • Rendering from props needs no JavaScript in the browser. The HTML the server produced is already the final answer.
  • State and browser APIs both require code that is still alive after the page loads, which is the definition of a client component.