Server Components in Next.js
Learn how Server Components work in Next.js: the default for every component, async, close to your data and secrets, and what they send to the browser.
- 01Tell which components run only on the serverKnow that layouts, pages and what they import are Server Components, unless an imported file starts with "use client" and so starts the client part.
- 02Read data and secrets while renderingWrite async components that wait for data and use a key from the server's environment, and say what reaches the browser.
- 03Read the errors at the edgeRecognize the build errors for state, Effects, event handlers, browser globals and class instances in a Server Component, and what each means.
A tea page that runs only on the server
In Next.js, a component you write runs on the server unless it's part of the client. A component like that is a Server Component: React's kind of component that runs only on the server. Its code never goes to the browser. How Next.js works followed one request through the tea shop, and The React you need for Next.js listed what each kind of component can do. Server Components, in the React course, shows the idea in React itself.
Here is the tea shop's home page. It's a real Next.js 16.4.3 project, built with next build and run with next start, and the result is a recording of what happened. Nothing in its files says "server". Open the Terminal tab.
app/page.tsximport { getTeas } from "./teas";import TeaCard from "./tea-card"; export default async function Home() { const teas = await getTeas(); console.log(`Home rendered with ${teas.length} teas`); return ( <main> <h1>Tea shop</h1> {teas.map((tea) => ( <TeaCard key={tea.id} name={tea.name} price={tea.price} /> ))} </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...Home rendered with 3 teasHome rendered with 3 teas✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentIn app/page.tsx, line 4 makes Home an async function, and line 5 waits for getTeas() from app/teas.ts, a stand-in for the shop's database. Line 6 prints a message with console.log. Lines 10 to 12 render a TeaCard for each tea; in app/tea-card.tsx, line 4 shows the tea's name and line 5 its price, like $4 a cup.
The Terminal tab shows Home rendered with 3 teas among the build's own lines, before ✓ Generating static pages: Home ran on the server, while the build made the page. It shows twice because, with Cache Components on, the build renders a page twice: a first time to fill its caches, and a second time to keep the result. (Caches are taught in the Rendering and caching stage.) Then ○ / says the page was prerendered as static content: its HTML was made ahead of time, and next start sends that copy.
Now open the Network tab. The first response is the page's HTML, with <h1>Tea shop</h1> and an <article> for each tea. Pick the second one, its RSC payload, asked for with rsc: 1. The RSC payload (RSC stands for React Server Components) is React's own description of what the Server Components rendered, with the props they pass to Client Components. React in the browser reads it, and inline scripts in the HTML carry it on a first visit. You can find "Green tea" and " a cup" in it. Both responses hold what the components rendered. Neither holds their code. The recording also searched every JavaScript file the page loads for Home rendered with, the text of line 6, and for a cup, from line 5 of TeaCard: neither is there. So the visitor's browser console never shows line 6's message. The code that prints it never arrives.
Home, TeaCard and getTeas run on the server. The browser gets what they rendered, in the HTML and the RSC payload: the heading, and each tea's name and price. Their code isn't in any JavaScript file the page loads.A newspaper's reporters work in the newsroom. They call their sources, read their notes and write. What reaches your door is the printed page: the stories, not the notes or the phone numbers. A few parts of the paper are made for you to work on at home, like the crossword.
- In real life: The newsroom, where reporters write the stories
- In Next.js: The server, where Server Components run
- In real life: The reporters' notes and their sources' phone numbers
- In Next.js: A Server Component's code, and values it never renders or passes on, like a key
- In real life: The printed paper at your door
- In Next.js: The HTML and the RSC payload: what the components rendered
- In real life: The crossword you fill in at home
- In Next.js: A Client Component: the part that works in the browser
In Next.js, components are Server Components unless a file with "use client" says otherwise. A Server Component runs only on the server, during the build or for a request. Its code, the code that loads its data, and any value it neither renders nor passes on stay there. The browser gets what it rendered, and the props it passes to Client Components.
Server Components are the default
You don't mark a Server Component. In the App Router, layouts and pages start as Server Components, and so does every component they import, unless the imported file starts with "use client": then the client part starts there. In the tea page, RootLayout is one because app/layout.tsx is a layout, Home is one because app/page.tsx is a page, and TeaCard is one because a Server Component imports it.
The client part starts at a file whose first line is "use client". That file's components, and everything the file imports, also go to the browser. The next lesson, Client Components, is about that line. One directive is easy to mix up with it: "use server" doesn't make Server Components. It marks Server Functions, functions that run on the server and that the browser can call. There is no directive for Server Components, because they're what you get without one.
Each row is one kind of file in the app folder: what it is, and why.
| The file | What it is | Why |
|---|---|---|
app/layout.tsx or app/page.tsx | A Server Component. | Layouts and pages start on the server. They need no directive. |
app/tea-card.tsx, imported by the page | A Server Component. | A component imported by a Server Component is one too, unless its file starts with "use client". |
A file that starts with "use client" | A Client Component. | The directive starts the client part: its code also goes to the browser. |
| A file imported by that Client Component | Part of the client too. | Everything a client file imports goes to the browser with it, directive or not. |
A file that starts with "use server" | Neither: it holds Server Functions. | "use server" marks functions the browser can call. It doesn't make Server Components. |
Server Components came with the App Router, in Next.js 13. The older Pages Router, in a pages folder, has none: there, every component's code goes to the browser and hydrates, and a page gets its server data from a function like getServerSideProps. Many projects still use it; Pages Router basics teaches enough to work in one.
Async components wait for their data
A Server Component can be an async function, like Home on line 4. Any Server Component can, not only pages. Here, each tea's stock comes from a small async component of its own, Stock. (A Client Component can't be async; it reads a promise with the use API instead.)
app/stock.tsximport { getStock } from "./supplier"; export default async function Stock({ id }: { id: string }) { const count = await getStock(id, process.env.SUPPLIER_KEY); return <>{count > 0 ? `${count} in stock` : "Sold out"}</>;}- Green tea: 12 in stock
- Mint tea: Sold out
- Lemon tea: 5 in stock
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/stock.tsx, line 3 makes Stock async. Line 4 waits for getStock, from app/supplier.ts, with the tea's id. Line 5 renders 12 in stock, or Sold out when the count is 0. In app/page.tsx, line 5 waits for the teas, and line 12 puts a Stock in each list item. The page is finished when every Stock has its answer. Open the Browser tab: Green tea has 12 left, Mint tea is sold out and Lemon tea has 5.
The build printed ○ /, because these stand-ins answer at once: their promises are settled before React has to wait, so nothing suspends. A component suspends when it waits for work that isn't finished yet, like a database query that hasn't answered. With Cache Components on, as in every new project, uncached work that suspends needs one of two things: a Suspense boundary that says what to show while it waits, or "use cache", which keeps the result. How long the wait takes isn't what matters. Try it: each button is a version of the stock page, built with real Next.js. Before you click, guess which one fails.
app/teas.ts// A stand-in for the shop's database. A real shop would read it here.export async function getTeas() { return [ { id: "green", name: "Green tea", price: 4, cost: 1.5 }, { id: "mint", name: "Mint tea", price: 3, cost: 1.1 }, { id: "lemon", name: "Lemon tea", price: 3, cost: 0.9 }, ];}$ next build▲ Next.js 16.4.3 (Turbopack)- Environments: .env- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentgetTeas on line 2 answers at once, so nothing suspends. Next.js renders the whole page during the build and prints ○ /.
The first version builds as ○ /. The second waits 50 ms for a promise nothing caches, and stops the build with Route "/": Next.js encountered uncached or runtime data during prerendering. The error lists three ways to fix it. The third waits just as long, but "use cache" on line 3 of app/teas.ts keeps the result: the build fills the cache and prints ○ /, with a Revalidate time of 15m beside it. The fourth takes the first fix: it moves the part that waits into TeaList and puts it inside Suspense. The build then prerenders the heading and Loading teas…, prints ◐ /, and the list is streamed in for each request. The Fetching and mutating data stage teaches these choices, and Rendering and caching teaches "use cache".
Reading data and secrets directly
A Server Component runs only on the server, so it can use what only the server has: a database, files on disk, a service inside your network, or a secret key. It reads them while it renders, with nothing in between.
Look at the stock example again. Line 4 of app/stock.tsx passes process.env.SUPPLIER_KEY to getStock. process.env holds the server's environment variables: settings the server gets from outside your code. Next.js loads them from the project's .env file, and line 1 of that file sets SUPPLIER_KEY. Click .env in the file tree to see it. In app/supplier.ts, line 3 refuses to answer without a key, the way a real supplier would. (The last lesson of this stage teaches .env files, and the NEXT_PUBLIC_ prefix for the few variables the browser may see.)
A client-rendered React app can't do this. Anyone can read the JavaScript a browser downloads, so the key can't be in it. You'd write an API on a server of your own that keeps the key and asks the supplier, and the browser would call that API after the page loads (Fetching data shows the browser side). In Next.js, Stock is that server code.
The recording checks that the key stayed on the server. It searched every JavaScript file the page loads for the key, sk_tea_7Hq2xP, and for the error text on line 3 of supplier.ts: neither is there. The page's markup and its RSC payload don't contain the key either. What they hold is what Stock rendered with it: 12 in stock.
Stock reads SUPPLIER_KEY on the server and asks for the count with it. The browser gets Green tea: 12 in stock. The recording looked for the key in the page's JavaScript, its markup and its RSC payload, and found it in none of them.A Server Component keeps its own code on the server. The files it imports stay there only while Server Components alone import them. If a Client Component imported app/supplier.ts, that file's code would go to the browser too. The server-only package makes the build stop when that happens; the last lesson of this stage teaches it.
What reaches the browser, and what never does
Two things reach the browser from a Server Component:
- What it rendered: the text and elements. They're in the HTML, for the first look, and in the RSC payload, for React.
- The props it passes to Client Components. A Client Component also runs in the browser, so its props have to get there. They travel in the RSC payload.
Its code never does, nor the files that only Server Components import, nor the values it reads but doesn't render or pass on. Props are the easy part to miss. Here the tea list gives each tea to AddToCart, the Client Component from earlier lessons, which only reads the tea's id. Pick each version and read the payload in the Network tab.
app/page.tsxNext.jsapp/page.tsximport { getTeas } from "./teas";import AddToCart from "./add-to-cart"; export default async function Home() { const teas = await getTeas(); return ( <main> <h1>Tea shop</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} <AddToCart tea={tea} /> </li> ))} </ul> </main> );}GET /?_rsc=200
- Sent with the request
rsc1
- Response headers
Varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encodingx-nextjs-cacheHITx-nextjs-prerender1x-nextjs-stale-time300Cache-Controls-maxage=31536000Content-Typetext/x-component
The body that arrived, as it arrived: the RSC payload, one row per line
#1:"$Sreact.fragment"
2:"/_next/static/chunks/1tq6ppe__m8m-.js"
3:I[39756,["$2"],"default"]
4:I[37457,["$2"],"default"]
8:I[97367,["$2"],"OutletBoundary"]
9:"$Sreact.suspense"
c:"ViewportBoundary"
d:I[97367,["$2"],"$c"]
f:"MetadataBoundary"
10:I[97367,["$2"],"$f"]
13:I[68027,["$2"],"default",1]
5:X
b:X
6:[["children",{"s":"__PAGE__","h":514,"d":{"r":["$","$1","c",{"children":["$L7",[["$","script","script-0",{"src":"/_next/static/chunks/3vhjj7ffizy12.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@a"}]}]]}],"p":true,"v":"$b"}}]]
12:X
14:X
15:X
0:{"P":null,"c":["",""],"q":"","i":false,"t":{"t":{"s":"","h":530,"d":{"r":["$","$1","c",{"children":[[["$","script","script-0",{"src":"/_next/static/chunks/1tq6ppe__m8m-.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],"p":true,"v":"$5"},"c":"$Q6"},"h":{"r":["$","$1","h",{"children":[null,["$","$Ld",null,{"children":"$Le"}],["$","$L10",null,{"children":[["$","div",null,{"hidden":true,"children":["$","$9",null,{"name":"Next.Metadata","children":"$L11"}]}],null]}],null]}],"p":true,"v":"$12"}},"m":"$undefined","G":["$13",[]],"S":true,"r":"$14","s":"$15","a":"$@16","l":"$undefined","p":"$undefined","d":"$undefined","b":"V2t28_2sDIDt9q1S714C4","u":"$@17"}
18:"/_next/static/chunks/3vhjj7ffizy12.js"
19:I[7870,["$2","$18"],"default"]
15:300
7:["$","main",null,{"children":[["$","h1",null,{"children":"Tea shop"}],["$","ul",null,{"children":[["$","li","green",{"children":["Green tea",": $",4," ",["$","$L19",null,{"tea":{"id":"green","name":"Green tea","price":4,"cost":1.5}}]]}],["$","li","mint",{"children":["Mint tea",": $",3," ",["$","$L19",null,{"tea":{"id":"mint","name":"Mint tea","price":3,"cost":1.1}}]]}],["$","li","lemon",{"children":["Lemon tea",": $",3," ",["$","$L19",null,{"tea":{"id":"lemon","name":"Lemon tea","price":3,"cost":0.9}}]]}]]}]]}]
e:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
a:null
11:[["$","title","0",{"children":"Tea shop"}]]
15:C
12:C
14:C
b:C
5:C
17:false
16:null
Line 12 passes each tea object. In the payload, each button's props hold id, name, price and cost, though AddToCart only reads id.
In the first version, the payload holds "tea":{"id":"green","name":"Green tea","price":4,"cost":1.5}. That's the price the shop pays for its tea, which no visitor should see, and it went to every browser. Nothing on the page shows it, but anyone who opens the developer tools can read it. In the second version, each button gets only {"id":"green"}. Give a Client Component only what it needs. The next lessons of this stage teach which values can be props at all, and how to pass data across safely.
tea, and its cost goes too. Pass tea.id, and only the id goes.Each row is one thing from the tea shop's Server Components: does it reach the browser, and how do the recordings show it?
| What | Does it reach the browser? | What the recordings show |
|---|---|---|
Home's and TeaCard's code | No. | None of the page's JavaScript files contains Home rendered with or a cup. |
teas.ts and supplier.ts | No. | Only Server Components import them, so they stay on the server. |
| What the components rendered | Yes, in the HTML and in the RSC payload. | Tea shop and 12 in stock are in both. |
| Props passed to a Client Component | Yes, whole, in the RSC payload. | Each tea's cost is there when the page passes the whole tea. |
| The supplier's key | No. | It's used on the server, and it's in neither the JavaScript, the page's markup nor the payload. |
A console.log message | No. | It's printed in the terminal that runs next build. |
One more thing never happens in the browser: a Server Component never hydrates. Hydration is React matching the Client Components to the HTML on the screen and attaching their event handlers. A Server Component has no code in the browser to match, so React uses what it rendered, from the payload, as it is.
What Server Components can't do
A Server Component runs on the server, gives back what it rendered, and is done. Its code never reaches the browser. So nothing that needs your code to keep running in the browser can go in it:
- State and Effects:
useState,useEffectand the other hooks that keep or react to changes after the page is on the screen. - Event handlers:
onClick,onChangeand the rest are functions the browser has to call. - Browser globals:
window,documentandlocalStorageexist only in the browser. - Props that aren't plain data: a Client Component's props travel in the payload, so they must be values React can send, not functions or class instances.
Next.js's docs add React context to the list. The browser can still do a lot on its own: a <details> element opens and closes, a link goes to its page, and a form can send its fields to a Server Function. The React you need for Next.js showed two of these errors. Here are all four kinds, in one tips page. Before you click, guess which versions build.
app/page.tsxNext.jsapp/page.tsxexport default function BrewingTips() { return ( <main> <h1>Brewing green tea</h1> <details> <summary>Show the tips</summary> <p>Use water at 80 °C and steep for 2 minutes.</p> </details> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentThe <details> element on lines 5 to 8 opens and closes by itself in the browser. No JavaScript of yours is needed, and the build prints ○ /.
Only the first version builds. Each of the others stops the build, and the message says what went wrong. Each row is one thing the tips page tried, what the recorded build printed, and why:
| What the Server Component does | What the build prints | Why |
|---|---|---|
Calls useState or useEffect | You're importing a module that depends on useState into a React Server Component module. | Hooks for state and Effects need the component to keep running in the browser. A Server Component renders on the server and never runs in the browser. |
Puts onClick on a <button> | Event handlers cannot be passed to Client Component props. | A handler is a function, and a Server Component's functions never leave the server. |
Reads window | ReferenceError: window is not defined | window is the browser's. On the server there's no window to read. |
| Passes a class instance as a prop | Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. | Props travel in the RSC payload as data. A class's methods are code, so they can't travel. |
Notice when each error comes. useState and useEffect stop the build while it compiles: Next.js reads each file's imports and knows they only work in Client Components. The other three stop it later, while it prerenders the page and the code runs on the server. Either way, the fix is to move the part that needs the browser into a Client Component and keep the rest on the server. That's the next lesson.
Planning a tea page
When you build a page, start on the server, and ask what each part needs. Most of a tea page is text from the database: names, prices, descriptions, stock. Only a few parts react to the visitor: a cup counter, a button that remembers a choice. Sort these parts of a tea page:
- Read the teas from the database while it renders.
- Use the supplier's key from
process.env. - Wait for data with
await. - Show tips that open and close with
<details>. - Count cups with
useState. - Change
document.titleinuseEffect. - Run code when a button is clicked.
- Read the window's width.
Sort each part: can a Server Component do it, or does it need a Client Component?
Every card matches a recording in this lesson: the stock page read data with a key and waited with await, the <details> version built, and the state, Effect, click and window versions stopped the build. Read the data on the server, pass small props, and keep the Client Components small.
Common mix-ups
- "A Server Component needs
"use server"." No. It needs no directive at all."use server"marks Server Functions. - "A Server Component runs again for every request." No.
Homeran duringnext build, andnext startsent the HTML the build kept. It runs again only when the page, or a part of it, is made again, like a part that reads the request. - "Server Components send nothing to the browser." No. What they render, and the props they pass to Client Components, are in the HTML and the RSC payload. Only their code stays behind.
- "Server Components hydrate too." No. Their code isn't in the browser. React uses what they rendered, from the payload, and hydrates only the Client Components.
- "A file that Server Components use is safe from the browser." No. If a Client Component imports the same file, its code goes to the browser too. Secrets belong in server-only modules and server environment variables.
- "Any await is fine in a Server Component." No. With Cache Components on, uncached work that suspends needs
Suspensearound it or"use cache", or the build stops and says so. Cached work, and work that never suspends, is fine.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
This is app/page.tsx. Where does the message on line 6 show up: in the terminal, or in the visitor's browser console?
import { getTeas } from "./teas";
import TeaCard from "./tea-card";
export default async function Home() {
const teas = await getTeas();
console.log(`Home rendered with ${teas.length} teas`);
return (
<main>
<h1>Tea shop</h1>
{teas.map((tea) => (
<TeaCard key={tea.id} name={tea.name} price={tea.price} />
))}
</main>
);
}
In the terminal that runs next build. Home is a Server Component, so it ran on the server while the page was prerendered, and the recorded build output shows Home rendered with 3 teas. The page's JavaScript doesn't contain the message, so the browser console never shows it.
This is app/tea-card.tsx from the first example. Is the text a cup from line 5 in the JavaScript the page loads?
export default function TeaCard({ name, price }: { name: string; price: number }) {
return (
<article>
<h2>{name}</h2>
<p>${price} a cup</p>
</article>
);
}
No. TeaCard is imported by Home, a Server Component, so it's a Server Component too. The recording searched every JavaScript file the page loads for a cup and didn't find it. The text reaches the browser only as rendered HTML, like $4 a cup.
The tips page wants to change the tab's title. The build stops. Which line does the error point at?
import { useEffect } from "react";
export default function BrewingTips() {
useEffect(() => {
document.title = "Brewing green tea";
}, []);
return (
<main>
<h1>Brewing green tea</h1>
<p>Use water at 80 °C and steep for 2 minutes.</p>
</main>
);
}
Line 1, the import of useEffect. Next.js reads the imports while it compiles, and the recorded build stops at ./app/page.tsx:1:10: You're importing a module that depends on useEffect into a React Server Component module.
AddToCart is a Client Component that only reads tea.id. Does the browser get each tea's cost?
import { getTeas } from "./teas";
import AddToCart from "./add-to-cart";
export default async function Home() {
const teas = await getTeas();
return (
<main>
<h1>Tea shop</h1>
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: ${tea.price} <AddToCart tea={tea} />
</li>
))}
</ul>
</main>
);
}
Yes. Line 12 passes the whole tea, and props travel whole, so the recorded payload holds "cost":1.5 for Green tea. Pass id={tea.id} instead, and the payload holds only {"id":"green"}.
With the uncached getTeas from the playground, which waits 50 ms, this page stops the build. Change it so the build passes, without "use cache". Which symbol does next build print for / then?
import { getTeas } from "./teas";
import Stock from "./stock";
export default async function Home() {
const teas = await getTeas();
return (
<main>
<h1>Tea shop</h1>
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: <Stock id={tea.id} />
</li>
))}
</ul>
</main>
);
}
import { Suspense } from "react";
import { getTeas } from "./teas";
import Stock from "./stock";
async function TeaList() {
const teas = await getTeas();
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: <Stock id={tea.id} />
</li>
))}
</ul>
);
}
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<Suspense fallback={<p>Loading teas…</p>}>
<TeaList />
</Suspense>
</main>
);
}
◐. The list moves into TeaList (lines 5 to 16), and Suspense on lines 22 to 24 says what to show while it waits. The build prerenders the heading and Loading teas… as a static shell, and the recorded build prints ◐ /: the list is streamed in for each request.
The build stops with Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Which line causes it?
import { Tea } from "./tea";
import AddToCart from "./add-to-cart";
export default function Home() {
const tea = new Tea("green", "Green tea");
return (
<main>
<h1>{tea.label()}</h1>
<AddToCart tea={tea} />
</main>
);
}
Line 9. AddToCart is a Client Component, so its props travel in the payload, and a class instance can't. The recorded build stops with: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Line 8 is fine: calling tea.label() on the server only renders text.
Check your understanding
For each question, ask where the code runs, and what the browser gets from it.
Question 1 of 7In a new Next.js app, which of these is a Server Component?
Choose an answer to see the explanation.
Question 2 of 7
Homecallsconsole.logwhile it renders. Where does the message show up?Choose an answer to see the explanation.
Question 3 of 7
Stockreadsprocess.env.SUPPLIER_KEYand renders how many cups are left. What does the browser get from it?Choose an answer to see the explanation.
Question 4 of 7What happens when you build this page?
Read the code, then predictTSXexport default function BrewingTips() { return ( <main> <h1>Brewing green tea</h1> <button onClick={() => alert("Use water at 80 °C and steep for 2 minutes.")}> Show the tips </button> </main> ); }Choose an answer to see the explanation.
Question 5 of 7A page awaits an uncached database query that suspends, with no
Suspensearound it and no"use cache". What doesnext builddo?Choose an answer to see the explanation.
Question 6 of 7In this page,
AddToCartonly readstea.id. What reaches the browser for each tea's button?Read the code, then predictTSXimport { getTeas } from "./teas"; import AddToCart from "./add-to-cart"; export default async function Home() { const teas = await getTeas(); return ( <main> <h1>Tea shop</h1> <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} <AddToCart tea={tea} /> </li> ))} </ul> </main> ); }Choose an answer to see the explanation.
Question 7 of 7Which of these can a Server Component do?
Choose an answer to see the explanation.
Key takeaways
- Layouts, pages and the components they import are Server Components, with no directive, unless an imported file starts with
"use client". A file with"use client"starts the client part;"use server"marks Server Functions. - A Server Component can be
asyncand wait for data while it renders. With Cache Components on, uncached work that suspends needsSuspenseor"use cache", and the build says so. - It can read databases, files and secrets directly. Its code, its data-loading code, and values it neither renders nor passes on stay on the server; its messages print in the server's terminal.
- The browser gets what it rendered, in the HTML and the RSC payload, and the props it passes to Client Components, whole. Pass only what a Client Component needs.
- It can't use state, Effects, event handlers, browser globals or props that aren't plain data, and it never hydrates. The build stops and names the problem.
Remember the one-liner.
A Server Component runs on the server and sends only its result; anything that needs the browser goes in a Client Component.
These links point at Next.js v16.4.3, the version every example was built with. The list of React APIs a Server Component file can't import, with useState, useEffect and createContext, is at lines 648 to 697 of react_server_components.rs, and the message it prints is on line 347. Why a page renders twice during the build is explained in the comment at lines 9138 to 9150 of app-render.tsx: the first render fills the caches, and the second is the one Next.js keeps. The Under the hood stage reads them.
Coming next: Client Components. It starts the client part with "use client", and shows what that line really marks.