cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Composing Server and Client Components

Fit Server and Client Components together in Next.js: server parts as children, props that can cross, promises read with use(), and context providers.

By the end, you can
  • 01
    Put server parts inside client partsPass Server Components to a Client Component as children or other props, and say why they still run only on the server.
  • 02
    Choose what crosses the boundaryTell which props can reach a Client Component, like Dates, Maps, promises, JSX and Server Functions, and read the errors for the ones that can't.
  • 03
    Share data and spot the mistakesStream data with a promise and use(), wrap server children in a context provider, and find the boundary mistakes that break a page.

A button that opens server-rendered details

The tea shop's Mint tea page has two kinds of work in it. The facts about the tea, where it's grown and how it tastes, come from the shop's database, so they belong on the server. A Show details button opens and closes them, and a button that reacts to clicks belongs in the browser. Server Components in Next.js and Client Components teach each kind on its own. The two fit together: a Client Component can hold a Server Component, as long as a Server Component puts it there.

This example is a real Next.js 16.4.3 project. It was built with next build and run with next start, and then real Chrome opened it and clicked the button. The result under the files is that recording.

Details, a Client Component, holding TeaFacts, a Server ComponentNext.js
app/
app/page.tsx
import Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() {  return (    <main>      <h1>Mint tea</h1>      <Details>        <TeaFacts />      </Details>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Mint tea
Show details

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

The project has three files that matter here:

  • app/tea-facts.tsx is a Server Component. On line 4 it waits for getTea("mint") from app/teas.ts, a stand-in for the database.
  • app/details.tsx is a Client Component: line 1 is "use client". Line 6 keeps open in state, the button on lines 9 to 11 flips it, and line 12 shows children only while it's open.
  • app/page.tsx, a Server Component, puts the two together. On lines 8 to 10, <TeaFacts /> sits between <Details> and </Details>, so it becomes Details's children.

Open the Steps tab and pick the second step: Chrome clicked Show details, and Grown in Morocco appeared. The list of requests for that step is empty. The facts weren't fetched when the button was clicked: they were already in the browser. Now open the Network tab. The first response is the page's HTML, and its visible markup doesn't contain Grown in, because the details start closed. Pick the second response, the page's RSC payload: the description of what the Server Components rendered, with the props for Client Components. The recording asked for it with the rsc: 1 header; on a first visit, Next.js sends the same payload inside the HTML response, in inline scripts. There the facts are, as plain text: Grown in and Morocco.

The recording also searched the JavaScript files the page loads. They contain Show details, from Details. They don't contain Grown in: TeaFacts's code never reached the browser, only what it rendered.

Two windows. Left, the server: MintPage, then TeaFacts running there, then its output, Grown in Morocco and Tastes fresh and sweet. Right, the browser: Details, shaded because its code runs there, with its Show details button, and the same two lines of output in its slot. An arrow carries the output from the server to the browser.On the serverserverMintPageTeaFacts runsGrown in MoroccoTastes fresh and sweetcomponent code stays here;what it renders travelsIn the browserbrowserDetailsShow detailsGrown in MoroccoTastes fresh and sweetonly the output arrives
TeaFacts runs on the server, and its output travels in the RSC payload. Details is shaded: its code runs in the browser too. It gets the output and decides where and when to show it; it never gets TeaFacts's code.
Real-life analogyA kitchen and a waiter

The kitchen cooks the soup and puts a lid on the bowl. The waiter carries it to the table and lifts the lid when the guest is ready. The waiter never cooks and never sees the recipe. Lifting the lid again doesn't send anyone back to the kitchen: the soup is already on the table.

In real life: The kitchen, where the soup is cooked
In Next.js: The server, where TeaFacts runs
In real life: The recipe, which never leaves the kitchen
In Next.js: TeaFacts's code, which never reaches the browser
In real life: The bowl of soup, under a lid
In Next.js: TeaFacts's output, in the RSC payload
In real life: The waiter, who lifts the lid at the table
In Next.js: Details, which shows or hides the output when clicked
The main idea

A Client Component can hold Server Components, as long as a Server Component puts them there. The Server Component renders on the server, and the Client Component gets its output, as children or another prop. Only data crosses from server to client: rendered output and serializable props, never server code.

Server Components as children of Client Components

TeaFacts sits inside Details on the page, yet it runs on the server. That's because React keeps two roles apart, and Next.js's guide to the boundary names them:

  • The owner of an element is the component whose code wrote it. <TeaFacts /> is written on line 9 of app/page.tsx, so its owner is MintPage.
  • The parent is the component that holds it when the page is put together. Details is the parent of TeaFacts.

The owner decides where a component runs. MintPage is a Server Component, so React runs TeaFacts on the server, while it renders the page. By the time Details runs, TeaFacts is already done, and what Details receives as children is its output. Details places it, hides it or shows it, but can't run it again.

Look at the imports. "use client" on line 1 of app/details.tsx pulls everything that file imports into the browser's JavaScript, as Client Components shows. Line 3 imports only React. TeaFacts is imported by app/page.tsx, a Server Component, so its code stays on the server.

children isn't special. Any prop can carry JSX across. Here Details takes the facts as a prop called facts:

The same page, passing TeaFacts as a prop called factsNext.js
app/
app/page.tsx
import Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() {  return (    <main>      <h1>Mint tea</h1>      <Details facts={<TeaFacts />} />    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Mint tea
Show details

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Line 8 of app/page.tsx passes facts={<TeaFacts />}, and line 12 of app/details.tsx shows facts while the details are open. The Steps tab shows the same result as before: the facts appear on the click, with no request. The recording searched the JavaScript again, and Grown in isn't in it. A Client Component can take several of these props, like a title and children, and put each one in its own place.

Each row asks where each component in the Mint tea page runs. Read it with the owner and parent in mind:

Who owns TeaFacts, and where each part runs
ComponentWhat it isWhere its code runs
MintPageA Server Component, and the owner of both Details and TeaFacts.Only on the server.
DetailsA Client Component, and the parent of TeaFacts.On the server for the first HTML, and in the browser.
TeaFactsA Server Component, owned by MintPage.Only on the server. The browser gets its output.

Props must be serializable

A Server Component and a Client Component run in different places, so props from one to the other have to travel. Next.js puts them in the RSC payload, as text. A value that can be turned into text and back into the same value is called serializable. React has its own format for this, and it carries more than JSON can: a Date comes back as a Date, not as a string.

Try it below. Each button is a version of the Mint tea page that passes a different kind of value to a Client Component, and each one was built with real Next.js. Before you click, guess which versions build.

Playground: which props can cross to a Client Component?
The Mint tea page, passing this kind of propNext.js
app/
app/page.tsx
import Stock from "./stock"; export default function MintPage() {  const harvested = new Date("2026-05-12");  const stock = new Map([["mint", 4], ["green", 0]]);  return (    <main>      <h1>Mint tea</h1>      <Stock harvested={harvested} stock={stock} />    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Mint tea
  • Harvested on 2026-05-12
  • Mint tea in stock: 4

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Try it yourself
A Date and a Map

Line 4 makes a Date and line 5 a Map, and line 9 passes both. React's payload can carry both, so Stock gets a real Date and a real Map: toISOString() and get() work in the browser too.

Every version was built with next build from Next.js 16.4.3; a failed build shows only the Terminal. Only app/page.tsx and the Client Component it renders change. Reset goes back to the first version.

Two versions build and two don't:

  • A Date and a Map cross as themselves. Stock calls harvested.toISOString() and stock.get("mint"), which only work on a real Date and a real Map. The recorded page shows Harvested on 2026-05-12 and Mint tea in stock: 4, and Chrome logged no error when React ran the same code in the browser.
  • A function can't cross. A function is code, and code from a Server Component stays on the server. The build stops while it prerenders the page: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". A prop whose name starts with on, like onAdd, gets a different message for the same reason, Event handlers cannot be passed to Client Component props, as The React you need for Next.js recorded.
  • A class instance can't cross either. The payload could carry its fields, but not its class, so the build stops: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Turn it into a plain object, like { name: tea.name, price: tea.price }, before you pass it.
  • A Server Function crosses as a reference: an id the browser can send back. Open its Steps tab and pick the click: Chrome sent a POST to / with a next-action header, and the Terminal tab shows what next start printed, addToCart ran on the server for mint. That text isn't in the page's JavaScript. The function ran on the server; only the request to run it came from the browser.

Next.js's TypeScript plugin, in your editor, only accepts a function-typed prop on a Client Component when its name is action or ends in Action, because a Server Function and an ordinary function have the same type. That's why the prop is called addAction. Each row is one kind of value, from React's reference for "use client" and the recordings above:

What can cross to a Client Component
What you passCan it cross?What the Client Component gets
A string, number, boolean, null or undefinedYes.The same value.
A plain object or an arrayYes, if everything inside can cross too.A copy with the same fields. Changing it in the browser doesn't change the server's.
A Date, Map or SetYes.A real Date, Map or Set. In the recording, toISOString() and get() work on them.
A promiseYes.A promise that settles when the server's does. The component reads it with use().
JSX, like <TeaFacts />Yes.What it rendered on the server, ready to place.
A Server Function, from a "use server" fileYes.A reference. Calling it sends a request, and the server runs the function.
Any other functionNo.The build stops: functions can't be passed directly to Client Components.
An object made from your own classNo.The build stops: only plain objects and a few built-ins can be passed.
Props crossing from a Server Component on the left to a Client Component on the right. A Date, a Map, a promise from getReviews, the element TeaFacts and the Server Function addToCart cross: they arrive as a real Date, a real Map, a promise for use, what TeaFacts rendered, and a reference to call. An arrow function and a new Tea object are crossed out: the build stops.The Server Component passesThe Client Component getsnew Date(…)a real Datenew Map(…)a real MapgetReviews()a promise, for use()<TeaFacts />what it renderedaddToCarta reference to call(amount) => …the build stopsnew Tea(…)the build stops
React's payload carries more than JSON can: a Date stays a Date, and a promise stays a promise. Five kinds of value cross here. An ordinary function and an object made from your own class can't, and the build stops.

Being able to cross doesn't make a value safe to send. Everything in a prop reaches the browser, even the fields the Client Component never shows. Server-only code and environment variables shows how that leaks secrets.

Passing a promise and reading it with use()

A promise is serializable too. The React you need for Next.js showed the pattern: a Server Component starts a request without await, passes the promise to a Client Component, and the Client Component reads it with use. Here is why you'd want it. The Mint tea page shows its reviews: they come from the server, and the list has a Show all button, which needs state.

The reviews, started on the server and read in a Client ComponentNext.js
app/
app/page.tsx
import { Suspense } from "react";import { getReviews } from "./reviews";import ReviewList from "./review-list"; export default function MintPage() {  const reviews = getReviews();  return (    <main>      <h1>Mint tea</h1>      <Suspense fallback={<p>Loading reviews…</p>}>        <ReviewList reviews={reviews} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Mint tea

Asha: Cool and fresh.

Show all 3 reviews

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Line 6 of app/page.tsx calls getReviews() without await, so reviews is a promise, and the page doesn't wait for it. Line 11 hands the promise to ReviewList, inside the Suspense on lines 10 to 12. In app/review-list.tsx, line 8 reads it with use(reviews). Until the promise settles, the component waits, and the Suspense shows Loading reviews…. Line 9 keeps showAll in state, and line 18 is the button. In app/reviews.ts, line 6 calls connection(), which waits for a real request, so the reviews are read for each visitor and not during the build.

Open the Terminal tab: ◐ /, prerendered as static HTML with dynamic server-streamed content. The heading and the fallback were made during the build. The Network tab shows the HTML as it arrived: the fallback inside <main>, and further down, in a hidden <div>, the first review and the button, which React swaps in. That markup holds only Asha's review. Yet in the Steps tab, clicking Show all 3 reviews shows Ben's and Chen's with no request at all. They came with the page, in the RSC payload, as the promise's value: the second response, the payload recorded with rsc: 1, holds all three reviews. On a first visit, that payload arrives inside the HTML response, in inline scripts. The JavaScript files don't contain Cool and fresh: the reviews are data, not code.

The other way to load them is the one you may know from client-rendered React apps: the Client Component asks for them itself, in an Effect, from a Route Handler, a route.ts file that answers requests with data. Step through both. Before you start, guess after which step each visitor sees the first review.

Playground: when do the reviews arrive?
Who does whatReviews as a promise
What the visitor seesafter step 1
tea-shop.example/

A blank page

React isn't running yet: clicks do nothing.

The page isn't complete yet.

Step 1 of 5
Browser: The visitor opens the Mint tea page

The browser asks the server for /.

Each step is one thing that happens, in order. The screens are from the recordings of the two examples in this section. Times aren't to scale. Reset goes back to the first journey's first step.

As a promise, the first review is on the screen after step 4, and nothing else is requested. Fetched in an Effect, it shows after step 6: the browser waits for the HTML, then the JavaScript, then hydration, React running the Client Components in the browser over the HTML that's already there, and only then starts a second request. Here is that version, recorded:

The reviews, fetched in an Effect after the page loadsNext.js
app/api/reviews/
app/review-list.tsx
"use client"; import { useEffect, useState } from "react"; type Review = { id: number; author: string; text: string }; export default function ReviewList() {  const [all, setAll] = useState<Review[] | null>(null);  const [showAll, setShowAll] = useState(false);  useEffect(() => {    fetch("/api/reviews")      .then((response) => response.json())      .then(setAll);  }, []);  if (all === null) return <p>Loading reviews…</p>;  const shown = showAll ? all : all.slice(0, 1);  return (    <section>      {shown.map((review) => (        <p key={review.id}>          {review.author}: {review.text}        </p>      ))}      {!showAll && <button onClick={() => setShowAll(true)}>Show all {all.length} reviews</button>}    </section>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Mint tea

Loading reviews…

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Lines 10 to 14 of app/review-list.tsx are the Effect, and line 15 shows Loading reviews… until it's done. The visible markup of the Network tab's HTML has only Loading reviews…, and the Steps tab lists two requests for one visit: the page, then GET /api/reviews. Next.js's docs still suggest fetching in the browser when the data depends on something only the browser knows, like what the visitor typed.

Context providers in a Client Component

The shop shows a cart count at the top of every page, and every Add button changes it. Context shares a value like that with every component below a provider, without passing props through each one. But context needs the browser: createContext and the state behind it only work in Client Components, and a Server Component can't read context. The answer is the same pattern as Details: a Client Component that provides the context and takes children.

A cart provider around the layout's childrenNext.js
app/
app/layout.tsx
import CartProvider from "./cart-provider";import CartCount from "./cart-count"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <CartProvider>          <CartCount />          {children}        </CartProvider>      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop

Cart: 2

Tea shop
  • Mint tea: $3 Add Mint tea
  • Green tea: $4 Add Green tea
Focus
the button “Add Green tea”
Scrolled
0 px down

No requests during this step: the page already had what it needed

app/cart-provider.tsx starts with "use client". Line 5 creates CartContext, line 8 keeps the count in state, and line 9 provides it with an add function around children. In app/layout.tsx, still a Server Component, lines 10 to 13 wrap the page in CartProvider, with CartCount above it. CartCount and AddTea read the context with use(CartContext) on line 7 of their files.

The page is the layout's children, so it stays a Server Component: line 5 of app/page.tsx still awaits getTeas() on the server. The Steps tab shows Chrome clicking Add Mint tea, then Add Green tea, and the count going to Cart: 1 and then Cart: 2, with no request. Two Client Components inside a Server Component page share one value from a provider above it.

A tree. RootLayout, a Server Component, renders CartProvider, a shaded Client Component. CartProvider holds two things: CartCount, shaded, and Home, the page, which stays a Server Component. Home renders two AddTea buttons, shaded. CartCount and AddTea both read the same cart from CartProvider.RootLayoutCartProviderCartCountHome (children)AddTea × 2reads countreads add()
Shaded parts are Client Components. CartProvider holds the cart in state. Home arrives as children, so it stays a Server Component, and the two AddTea buttons inside it still read the provider's cart, as CartCount does.
Put providers as deep as you can

CartProvider wraps the content of <body>, not the whole <html> document. Next.js's docs recommend rendering providers as deep in the tree as possible, because that makes it easier for Next.js to optimize the static parts of your Server Components. A provider that only one section needs can go in that section's layout.

Common mistakes at the boundary

The most common mistake is to import a Server Component into a Client Component's file, instead of passing it in. It looks like the same page. Try four ways of putting TeaFacts inside Details; each opens on the click, in the Steps tab, unless its build failed.

Playground: where does TeaFacts end up?
The Mint tea page, with this way of putting TeaFacts in DetailsNext.js
app/
app/page.tsx
import Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() {  return (    <main>      <h1>Mint tea</h1>      <Details>        <TeaFacts />      </Details>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Mint tea
Hide details
  • Grown in Morocco
  • Tastes fresh and sweet
Focus
the button “Hide details”
Scrolled
0 px down

No requests during this step: the page already had what it needed

Try it yourself
Passed as children

The page puts <TeaFacts /> inside <Details> on lines 8 to 10. TeaFacts runs on the server, and Details gets its output. The click asks the server for nothing, and the browser's JavaScript doesn't contain Grown in.

Every version was built with next build from Next.js 16.4.3, and the click was made in real Chrome. A failed build shows only the Terminal. Reset goes back to the first version.

What happened in each version:

  • Passed as children, the pattern from the start of this lesson, works, and keeps TeaFacts on the server.
  • Imported, no data: the import on line 4 of app/details.tsx makes TeaFacts client code, with or without a directive of its own. It still works, because this TeaFacts only returns fixed text, but its code is in the browser's JavaScript now: the recording found Grown in there.
  • Imported, async: the build passes and the page loads. On the click, React tries to render an async component in the browser, and the page breaks: React's error #482 says an async Client Component isn't supported, and Next.js shows This page couldn’t load. This bug waits for a visitor to click.
  • Imported, reads a cookie: cookies() from next/headers only works on the server, so Turbopack stops the build. Its message says one of the module's parents is marked with "use client", so the module is a Client Component too, and its import trace lists app/details.tsx.

Each row is one mistake, what you'll see, and the fix. The recordings in this lesson show every one of them.

Boundary mistakes and their fixes
The mistakeWhat happensThe fix
A Client Component's file imports a Server Component.It becomes client code. Its code goes to the browser, an async one breaks the page, and one that reads a cookie stops the build.Let a Server Component pass it in, as children or another prop.
A Server Component passes an ordinary function as a prop.The build stops while it prerenders the page.Move the function into the Client Component's file, or make it a Server Function.
A context provider's file has no "use client".The build stops: createContext only works in Client Components.Start the provider's file with "use client".
A Server Component uses Menu.Item, a property of a client component.The build passes. In the browser, Menu.Item is undefined, and the page breaks.Export MenuItem by name, and import it by name.
A page awaits request-time data outside Suspense.The build stops: uncached or runtime data during prerendering.Pass the promise down, and read it with use() inside Suspense.

The whole Mint tea page

Here is the page with every pattern from this lesson: the cart provider in the layout, an Add button, the details with the facts as children, and the reviews as a promise.

The Mint tea page, with everything from this lessonNext.js
app/
app/page.tsx
import { Suspense } from "react";import AddTea from "./add-tea";import Details from "./details";import TeaFacts from "./tea-facts";import ReviewList from "./review-list";import { getReviews } from "./reviews"; export default function MintPage() {  const reviews = getReviews();  return (    <main>      <h1>Mint tea</h1>      <AddTea name="Mint tea" />      <Details>        <TeaFacts />      </Details>      <Suspense fallback={<p>Loading reviews…</p>}>        <ReviewList reviews={reviews} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop

Cart: 1

Mint tea
Add Mint tea
Hide details
  • Grown in Morocco
  • Tastes fresh and sweet

Asha: Cool and fresh.

Ben: Great with honey.

Chen: My evening tea.

Focus
nothing: the page itself
Scrolled
0 px down

No requests during this step: the page already had what it needed

MintPage stays a Server Component from top to bottom. Line 9 starts the reviews, line 13 renders the AddTea button, lines 14 to 16 give Details the facts, and lines 17 to 19 stream the reviews. The build prints ◐ /. In the Steps tab, three clicks change the cart, open the facts and show every review, and none of them makes a request. The page's JavaScript contains Show details and Show all, but not Grown in or Cool and fresh.

Sort the page's parts. Which ones send their code to the browser?

Server Component or Client Component?
  • MintPage in app/page.tsx
  • TeaFacts, passed to Details as children
  • Details, the button that opens the facts
  • ReviewList, which reads the promise with use()
  • CartProvider, around the layout's children
  • RootLayout, which renders CartProvider
  • CartCount, which reads the cart's context
  • AddTea, the Add Mint tea button
Try it yourself
0 of 8 correct

Sort each part of the Mint tea page: does its code stay on the server, or does it also run in the browser?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A Server Component inside a Client Component becomes a Client Component." No. Its owner decides. Passed in by a Server Component, it runs on the server, and the Client Component only places its output. Only an import from a client file makes it client code.
  • "A provider in the root layout turns the whole app into Client Components." No. The pages arrive as children and stay Server Components. The shop's Home still awaited getTeas() on the server.
  • "Props are sent as JSON." No. React's format also carries Date, Map, Set, promises, JSX and Server Function references. It doesn't carry ordinary functions or objects made from your own classes.
  • "Passing a promise means the browser fetches the data." No. The server reads it, and the value arrives with the page. In the recording, showing all three reviews sent no request.
  • "A Server Function prop sends the function to the browser." No. The browser gets a reference. Calling it sends a POST, and the server runs the function.
  • "If it builds, the boundary is fine." No. The imported async TeaFacts and Menu.Item both built, then broke the page when a visitor clicked.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead a Map after it crossed

The Mint tea page passes this Client Component stock={new Map([["mint", 4], ["green", 0]])}. What does the second list item show?

Starter codeTSX
"use client";

type Props = { harvested: Date; stock: Map<string, number> };

export default function Stock({ harvested, stock }: Props) {
  return (
    <ul>
      <li>Harvested on {harvested.toISOString().slice(0, 10)}</li>
      <li>Mint tea in stock: {stock.get("mint")}</li>
    </ul>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the server text in the JavaScript

    TeaFacts renders Grown in followed by the tea's origin. With this page, do the JavaScript files the browser loads contain the text Grown in?

    Starter codeTSX
    import Details from "./details";
    import TeaFacts from "./tea-facts";
    
    export default function MintPage() {
      return (
        <main>
          <h1>Mint tea</h1>
          <Details>
            <TeaFacts />
          </Details>
        </main>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the price

      This page passes formatPrice to Price, a Client Component with a One more button, and the build stops. Change the files so it builds. Then, what does the paragraph show after one click on One more?

      Starter codeTSX
      import Price from "./price";
      
      export default function MintPage() {
        return (
          <main>
            <h1>Mint tea</h1>
            <Price price={3} formatPrice={(amount) => `$${amount}.00`} />
          </main>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the provider's missing line

        The shop's build fails with this file as app/cart-provider.tsx: You're importing a module that depends on createContext into a React Server Component module. Which line is missing, and where?

        Starter codeTSX
        import { createContext, useState, type ReactNode } from "react";
        
        export const CartContext = createContext({ count: 0, add: () => {} });
        
        export default function CartProvider({ children }: { children: ReactNode }) {
          const [count, setCount] = useState(0);
          return <CartContext value={{ count, add: () => setCount(count + 1) }}>{children}</CartContext>;
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeStream the reviews

          With this page, the build stops: Route "/": Next.js encountered uncached or runtime data during prerendering. Change it, and ReviewList, so the heading shows at once and the reviews stream in, with the Show all button still working. Which symbol does the build then print for /?

          Starter codeTSX
          import { getReviews } from "./reviews";
          import ReviewList from "./review-list";
          
          export default async function MintPage() {
            const reviews = await getReviews();
            return (
              <main>
                <h1>Mint tea</h1>
                <ReviewList reviews={reviews} />
              </main>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFix Menu.Item

            The home page writes <Menu><Menu.Item>Teas</Menu.Item>…</Menu>. The build passes, but clicking Menu breaks the page with React's error #130: Element type is invalid. What should app/menu.tsx export instead of Menu.Item?

            Starter codeTSX
            "use client";
            
            import { useState, type ReactNode } from "react";
            
            export default function Menu({ children }: { children: ReactNode }) {
              const [open, setOpen] = useState(false);
              return (
                <nav>
                  <button onClick={() => setOpen(!open)}>Menu</button>
                  {open && <ul>{children}</ul>}
                </nav>
              );
            }
            
            Menu.Item = function MenuItem({ children }: { children: ReactNode }) {
              return <li>{children}</li>;
            };
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask who owns each component, and what has to travel between the server and the browser.

              The composing components quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Details is a Client Component. MintPage puts <TeaFacts /> inside <Details>. Where does TeaFacts run?

                Choose an answer to see the explanation.

              2. Question 2 of 7This page passes two props to Stock, a Client Component. What does the build do?

                Read the code, then predictTSX
                import Stock from "./stock";
                
                export default function MintPage() {
                  const harvested = new Date("2026-05-12");
                  const stock = new Map([["mint", 4], ["green", 0]]);
                  return (
                    <main>
                      <h1>Mint tea</h1>
                      <Stock harvested={harvested} stock={stock} />
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7Which prop stops the build when a Server Component passes it to a Client Component?

                Choose an answer to see the explanation.

              4. Question 4 of 7In ReviewList, what does use(reviews) do?

                Choose an answer to see the explanation.

              5. Question 5 of 7The reviews page builds as ◐ /. What does the first chunk of its HTML show?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does CartProvider start with "use client"?

                Choose an answer to see the explanation.

              7. Question 7 of 7Details imports the async TeaFacts itself. What happens?

                Read the code, then predictTSX
                "use client";
                
                import { useState } from "react";
                import TeaFacts from "./tea-facts";
                
                export default function Details() {
                  const [open, setOpen] = useState(false);
                  return (
                    <section>
                      <button onClick={() => setOpen(!open)}>
                        {open ? "Hide details" : "Show details"}
                      </button>
                      {open && <TeaFacts />}
                    </section>
                  );
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • A Client Component can hold Server Components when a Server Component passes them in, as children or any other prop. They run on the server, and the Client Component gets their output.
              • The owner, the component whose code writes the JSX, decides where it runs. The parent only places the output.
              • Props must be serializable by React: values, plain objects, Date, Map, Set, promises, JSX and Server Functions cross. Ordinary functions and class instances stop the build.
              • Pass a promise without await and read it with use() inside Suspense: the shell is built ahead, and the data arrives with the page, with no second request.
              • Context needs a Client Component provider that takes children. The pages inside it stay Server Components.
              • Importing a Server Component into a client file makes it client code. Some of these mistakes stop the build; others only break when a visitor clicks.

              Remember the one-liner.
              Pass server parts into client parts, don't import them: only data crosses the boundary, never server code.

              Coming next: Server-only code and environment variables. It keeps secrets on the server with server-only, .env files and the NEXT_PUBLIC_ prefix, and shows how they still leak.

              CompleteFrontend Clear concepts. Working examples.