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.
- 01Put 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.
- 02Choose 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.
- 03Share 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.
app/page.tsximport Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() { return ( <main> <h1>Mint tea</h1> <Details> <TeaFacts /> </Details> </main> );}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.tsxis a Server Component. On line 4 it waits forgetTea("mint")fromapp/teas.ts, a stand-in for the database.app/details.tsxis a Client Component: line 1 is"use client". Line 6 keepsopenin state, the button on lines 9 to 11 flips it, and line 12 showschildrenonly 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'schildren.
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.
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.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
TeaFactsruns - 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
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 ofapp/page.tsx, so its owner isMintPage. - The parent is the component that holds it when the page is put together.
Detailsis the parent ofTeaFacts.
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:
app/page.tsximport Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() { return ( <main> <h1>Mint tea</h1> <Details facts={<TeaFacts />} /> </main> );}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:
| Component | What it is | Where its code runs |
|---|---|---|
MintPage | A Server Component, and the owner of both Details and TeaFacts. | Only on the server. |
Details | A Client Component, and the parent of TeaFacts. | On the server for the first HTML, and in the browser. |
TeaFacts | A 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.
app/page.tsximport 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> );}- 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.
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.
Two versions build and two don't:
- A Date and a Map cross as themselves.
Stockcallsharvested.toISOString()andstock.get("mint"), which only work on a realDateand a real Map. The recorded page showsHarvested on 2026-05-12andMint 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, likeonAdd, 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
POSTto/with anext-actionheader, and the Terminal tab shows whatnext startprinted,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 you pass | Can it cross? | What the Client Component gets |
|---|---|---|
A string, number, boolean, null or undefined | Yes. | The same value. |
| A plain object or an array | Yes, 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 Set | Yes. | A real Date, Map or Set. In the recording, toISOString() and get() work on them. |
| A promise | Yes. | 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" file | Yes. | A reference. Calling it sends a request, and the server runs the function. |
| Any other function | No. | The build stops: functions can't be passed directly to Client Components. |
| An object made from your own class | No. | The build stops: only plain objects and a few built-ins can be passed. |
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.
app/page.tsximport { 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> );}Asha: Cool and fresh.
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.
Reviews as a promiseafter step 1A blank page
React isn't running yet: clicks do nothing.
The page isn't complete yet.
The browser asks the server for /.
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:
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> );}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.
app/layout.tsximport 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
Cart: 2
- Mint tea: $3
- Green tea: $4
- 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.
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.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.
app/page.tsximport Details from "./details";import TeaFacts from "./tea-facts"; export default function MintPage() { return ( <main> <h1>Mint tea</h1> <Details> <TeaFacts /> </Details> </main> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- 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
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.
What happened in each version:
- Passed as children, the pattern from the start of this lesson, works, and keeps
TeaFactson the server. - Imported, no data: the import on line 4 of
app/details.tsxmakesTeaFactsclient code, with or without a directive of its own. It still works, because thisTeaFactsonly returns fixed text, but its code is in the browser's JavaScript now: the recording foundGrown inthere. - 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()fromnext/headersonly 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 listsapp/details.tsx.
Each row is one mistake, what you'll see, and the fix. The recordings in this lesson show every one of them.
| The mistake | What happens | The 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.
app/page.tsximport { 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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
Cart: 1
- 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?
MintPageinapp/page.tsxTeaFacts, passed toDetailsas childrenDetails, the button that opens the factsReviewList, which reads the promise withuse()CartProvider, around the layout's childrenRootLayout, which rendersCartProviderCartCount, which reads the cart's contextAddTea, the Add Mint tea button
Sort each part of the Mint tea page: does its code stay on the server, or does it also run in the browser?
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
childrenand stay Server Components. The shop'sHomestill awaitedgetTeas()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
TeaFactsandMenu.Itemboth 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.
The Mint tea page passes this Client Component stock={new Map([["mint", 4], ["green", 0]])}. What does the second list item show?
"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>
);
}
Mint tea in stock: 4. The Map arrives as a real Map, so stock.get("mint") returns 4. The recorded page shows exactly that line, under Harvested on 2026-05-12.
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?
import Details from "./details";
import TeaFacts from "./tea-facts";
export default function MintPage() {
return (
<main>
<h1>Mint tea</h1>
<Details>
<TeaFacts />
</Details>
</main>
);
}
No. TeaFacts is imported by the page, a Server Component, and passed to Details as children. The browser gets its output in the RSC payload, not its code. The recording searched the page's JavaScript and didn't find Grown in.
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?
import Price from "./price";
export default function MintPage() {
return (
<main>
<h1>Mint tea</h1>
<Price price={3} formatPrice={(amount) => `$${amount}.00`} />
</main>
);
}
"use client";
import { useState } from "react";
function formatPrice(amount: number) {
return `$${amount}.00`;
}
export default function Price({ price }: { price: number }) {
const [count, setCount] = useState(1);
return (
<p>
{count} × {formatPrice(price)} = {formatPrice(price * count)}{" "}
<button onClick={() => setCount(count + 1)}>One more</button>
</p>
);
}
formatPrice now lives in the Client Component's file, so it's client code, and only the number 3 crosses. The recorded page starts at 1 × $3.00 = $3.00, and after one click on One more it shows 2 × $3.00 = $6.00, with no request.
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?
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>;
}
"use client";
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>;
}
"use client"; at the top of app/cart-provider.tsx. Without it, the root layout imports the provider as a Server Component, and the recorded build stops: You're importing a module that depends on createContext into a React Server Component module.
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 /?
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>
);
}
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>
);
}
The page starts getReviews() without await and passes the promise to ReviewList inside Suspense; ReviewList reads it with use(reviews). The recorded build prints ◐ /: the heading and Loading reviews… are built ahead, and the reviews stream in.
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?
"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>;
};
"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>
);
}
export function MenuItem({ children }: { children: ReactNode }) {
return <li>{children}</li>;
}
// app/page.tsx
import Menu, { MenuItem } from "./menu";
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<Menu>
<MenuItem>Teas</MenuItem>
<MenuItem>Gift boxes</MenuItem>
</Menu>
</main>
);
}
Export MenuItem as a named export and import it with import Menu, { MenuItem } from "./menu". A Server Component that imports a client file gets references to its exports, so Menu.Item was undefined in the browser, and React's error #130 broke the page. With the named export, clicking Menu shows Teas and Gift boxes.
Check your understanding
For each question, ask who owns each component, and what has to travel between the server and the browser.
Question 1 of 7
Detailsis a Client Component.MintPageputs<TeaFacts />inside<Details>. Where doesTeaFactsrun?Choose an answer to see the explanation.
Question 2 of 7This page passes two props to
Stock, a Client Component. What does the build do?Read the code, then predictTSXimport 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.
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.
Question 4 of 7In
ReviewList, what doesuse(reviews)do?Choose an answer to see the explanation.
Question 5 of 7The reviews page builds as
◐ /. What does the first chunk of its HTML show?Choose an answer to see the explanation.
Question 6 of 7Why does
CartProviderstart with "use client"?Choose an answer to see the explanation.
Question 7 of 7
Detailsimports the asyncTeaFactsitself. 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
childrenor 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
awaitand read it withuse()insideSuspense: 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.