The React you need for Next.js
A refresher on the React that Next.js code uses every day: components, state, Effects, Suspense, Server Components and form actions, with live examples.
- 01Spot the React in a Next.js pageName the React behind each part of a page: components, props, state, Effects, Suspense,
use, Server Functions and form actions. - 02Tell what runs whereSay which React features need a Client Component, which work in a Server Component, and what one can pass to the other.
- 03Find the lesson behind each ideaKnow which React lesson teaches each part in full, so you can read it before the Next.js lessons that use it.
A Next.js page is React you already know
Every page, layout and button in a Next.js app is a React component. Next.js adds files, folders and a server around them, but the code inside is React. This lesson is a quick refresher on the React that Next.js code uses every day, and on the few places where Next.js changes how it behaves. It doesn't teach React from the start: each section links to the React lessons that do.
Here is one page of the tea shop from Why Next.js?, as a real Next.js 16.4.3 project. It was built with next build, run with next start, and what it sent back was recorded. Click a file in the tree to read it.
app/page.tsximport { getTeas } from "./teas";import TeaCard from "./tea-card"; export default async function Home() { const teas = await getTeas(); return ( <main> <h1>Tea shop</h1> {teas.map((tea) => ( <TeaCard key={tea.id} name={tea.name} price={tea.price} /> ))} </main> );}$4 a cup
$3 a cup
$3 a cup
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Almost every line is React:
Home, on line 4 ofapp/page.tsx, is a component: a function that returns markup.- Line 5 waits for
getTeas(), becauseHomeis anasyncServer Component. It runs only on the server. - Line 10 makes a
TeaCardfor each tea with map and akey, and hands it two props:nameandprice. - In
app/tea-card.tsx, line 6 keeps a count in state, and line 11 changes it in an event handler. - Line 1 of
app/tea-card.tsxis"use client". That's the one line here that isn't everyday React. It marks where the part of the app that also runs in the browser begins, so the card can keep state and handle clicks.
Open the Network tab: the HTML already holds the heading and the three cards, each with an Add to cart button. Both components ran on the server to make that HTML. Only TeaCard's code also goes to the browser, so the button works there.
Home is a Server Component: it can be async and wait for data, and it runs only on the server. TeaCard starts with "use client", so it can keep state and handle clicks. The page hands it two props.A cook who moves to a restaurant keeps the same recipes. What's new is where each step happens: most of the cooking in the back room, and a few last touches at the counter, where the guest can watch and ask for changes.
- In real life: Recipes you already know
- In Next.js: Components, props, state, Effects, Suspense
- In real life: A kitchen with a back room and a front counter
- In Next.js: The server and the browser
- In real life: Dishes cooked in the back, ready before the guest sees them
- In Next.js: Server Components, which can wait for data
- In real life: The last touch made at the counter, in front of the guest
- In Next.js: Client Components, which handle clicks and state
Next.js code is React code. Components, props, state, Effects, Suspense, use and form actions work as the React course teaches them. What Next.js adds is where each one runs: components start as Server Components on the server, and a file that starts with "use client" marks the part that also runs in the browser, where state, clicks and Effects live.
If any of these feel new, read its React lesson first. Here they all are, in the order this lesson reminds you of them:
Components, props and state
Here is the tea card again, this time as a client-rendered React example, like a new Vite app: React draws it in the browser, so it runs right in this page. Click Add to cart on Mint tea twice, then once on Green tea.
import TeaCard from "./TeaCard.jsx"; const teas = [ { id: "green", name: "Green tea", price: 4 }, { id: "mint", name: "Mint tea", price: 3 }, { id: "lemon", name: "Lemon tea", price: 3 },]; export default function App() { return ( <main> <h1>Tea shop</h1> {teas.map((tea) => ( <TeaCard key={tea.id} name={tea.name} price={tea.price} /> ))} </main> );}Mint tea's button says In cart: 2, and Green tea's says In cart: 1. Lemon tea's still says Add to cart. A quick reminder of why:
- A component is a function whose name starts with a capital letter and that returns JSX.
Appon line 9 is one, and so isTeaCardin theTeaCard.jsxtab. - Props are the inputs a parent hands a child. Line 14 hands each card a
nameand aprice, and line 3 ofTeaCard.jsxtakes them out with destructuring. A component never changes its own props. - State is a component's memory. Line 4 of
TeaCard.jsxgives each card its owncount, so the three counts don't mix. CallingsetCounton line 9 asks React to run the component again with the new value, and React updates the button. - The
keyon line 14 tells React which card is which when the list changes (Rendering lists).
What changes in Next.js. The card is the same code, with types and one more line. In the overview's app/tea-card.tsx, line 1 is "use client": state and clicks only work in Client Components. The list itself moved into Home, a Server Component, which hands each card its props. Props that go from a Server Component to a Client Component travel from the server to the browser, so they have to be data: text, numbers, plain objects and arrays. The section on Server Components shows what else can travel. Sharing state and Thinking in React teach where state should live; in Next.js, that place has to be a Client Component.
Effects, and when you don't need one
An Effect is code that React runs after a component is on the screen, to keep it in step with something outside React: a timer, a browser event, a connection. This one keeps a number in step with the size of the window. The result frame is a small window of its own.
import { useEffect, useState } from "react"; export default function WindowWidth() { const [width, setWidth] = useState(null); useEffect(() => { function measure() { setWidth(window.innerWidth); } measure(); window.addEventListener("resize", measure); return () => window.removeEventListener("resize", measure); }, []); return <p>{width === null ? "Measuring your window…" : `Your window is ${width} px wide.`}</p>;}Line 4 starts width at null. After React has put the paragraph on the screen, it runs the Effect on lines 6 to 13: line 10 measures the window, and line 11 measures it again whenever it's resized. Line 12 returns a cleanup function, which React calls when the component goes away, so the listener doesn't stay behind. The empty array on line 13 says the Effect depends on nothing, so it runs once. The result says Your window is … px wide. with the result frame's width; make your browser window narrower to watch it change.
What changes in Next.js. Effects run only in the browser. Next.js makes a Client Component's HTML on the server without running its Effects, because the server has no screen and no window. Here is the same component in a Next.js page. Open the Browser tab:
app/window-width.tsx"use client"; import { useEffect, useState } from "react"; export default function WindowWidth() { const [width, setWidth] = useState<number | null>(null); useEffect(() => { function measure() { setWidth(window.innerWidth); } measure(); window.addEventListener("resize", measure); return () => window.removeEventListener("resize", measure); }, []); return <p>{width === null ? "Measuring your window…" : `Your window is ${width} px wide.`}</p>;}Measuring your window…
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
The recorded page says Measuring your window…: the HTML was made during next build, with width still null from line 6 of app/window-width.tsx, so line 17 chose the first text. In the browser, React runs the Effect after hydration: React matches the Client Components to the HTML that's already on the screen and attaches their event handlers. Then the width appears. A Server Component can't have Effects at all: they need a browser to run in.
Many Effects aren't needed. An Effect that only works out a value from props or state does extra work: React draws the page, runs the Effect, then draws it again with the new value. Work the value out while rendering instead:
import { useState } from "react"; const prices = { green: 4, mint: 3, lemon: 3 }; export default function App() { const [cart, setCart] = useState(["green", "mint"]); const total = cart.reduce((sum, id) => sum + prices[id], 0); return ( <> <p> {cart.length} teas, total ${total} </p> <button onClick={() => setCart([...cart, "lemon"])}>Add lemon tea</button> </> );}Line 7 adds up the prices each time App runs, so total is always right: 2 teas, total $7, and after a click on Add lemon tea, 3 teas, total $10. This is the version to avoid; it isn't a whole app, so it doesn't run on its own:
// Avoid: an Effect that copies one state into another
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(cart.reduce((sum, id) => sum + prices[id], 0));
}, [cart]);In Next.js there's one more Effect you rarely need: the one that loads a page's data. In a client-rendered React app, a component can start a request in an Effect after it's on the screen (Fetching data shows that, and better ways). In Next.js, an async Server Component waits for the data on the server, like Home in the overview, so the data is already in the HTML. Effects and You might not need an Effect teach both sides in full.
Suspense and the use API
Suspense shows a fallback, like a loading message, while a part of the page waits for something. The use API reads the value of a promise inside a component. If the promise hasn't settled yet, the component waits, and the nearest Suspense shows its fallback. Press Run again on the result to watch it.
import { Suspense, use } from "react";import { getTeas } from "./api.js"; const teasPromise = getTeas(); function TeaList({ teasPromise }) { const teas = use(teasPromise); return ( <ul> {teas.map((tea) => ( <li key={tea.id}>{tea.name}</li> ))} </ul> );} export default function App() { return ( <main> <h1>Tea shop</h1> <Suspense fallback={<p>Loading teas…</p>}> <TeaList teasPromise={teasPromise} /> </Suspense> </main> );}Line 4 starts the request once, outside any component, and keeps its promise. In the api.js tab, line 9 answers after 600 ms. Line 7 reads the promise with use. For the first 600 ms, the teas aren't there yet, so the Suspense on line 21 shows Loading teas…. The heading on line 20 is outside it, so it shows at once. When the promise settles, React shows the list.
What changes in Next.js. Two things. First, the server can start the request and hand the promise to a Client Component, which reads it with use. Second, the server streams the page: it sends the HTML around the Suspense first, with the fallback, and the waiting part later in the same response. Here, getTeas in app/teas.ts calls connection() on line 5, which waits for a real request, so the teas are read for each visitor instead of during the build.
app/page.tsximport { Suspense } from "react";import { getTeas } from "./teas";import TeaList from "./tea-list"; export default function Home() { const teasPromise = getTeas(); return ( <main> <h1>Tea shop</h1> <Suspense fallback={<p>Loading teas…</p>}> <TeaList teasPromise={teasPromise} /> </Suspense> </main> );}- Green tea
- Mint tea
- Lemon tea
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 getTeas() without await, and line 11 hands the promise to TeaList. Line 8 of app/tea-list.tsx reads it with use, as in the live example. Open the Terminal tab: ◐ / means prerendered as static HTML with dynamic server-streamed content. Here, the heading and the fallback were made during the build, and the teas are streamed in for each request. The Network tab shows the HTML as it arrived: the fallback Loading teas… near the top, and the teas further down, which React swaps in. The Browser tab shows the page after the swap.
Suspense fallback. When the teas are ready, the server sends them in the same response, and they take the fallback's place.Suspense, The use API and Streaming teach these in full. In Next.js, a loading.tsx file is a Suspense around a whole page; Loading UI shows it.
Server Components and Server Functions
A Server Component is a React component that runs only on the server. It can be async, wait for data, and read things the browser must never see, like a database password. Its code never goes to the browser; only what it rendered does. A Client Component is one whose file starts with "use client", or is imported by such a file. Its HTML is made on the server too, and then its code runs in the browser. In Next.js, components start as Server Components.
A Server Function is an async function in a file that starts with "use server". It runs on the server, but the browser can call it: as a form's action, or from a Client Component. The browser never gets its code, only a reference to it.
Each row is one thing you might want a component to do, and whether each kind of component can do it:
| What you want to do | In a Server Component | In a Client Component |
|---|---|---|
Wait for data with await | Yes. It can be an async function. | No. It can read a promise with use instead. |
Keep state with useState | No. The build stops with an error. | Yes. |
Handle a click with onClick | No. A handler is a function, and a function can't reach the browser. | Yes. |
| Run an Effect | No. Effects run only in the browser. | Yes, in the browser, after React has put it on the screen. |
| Read a database or a secret key | Yes. Its code never reaches the browser; only what it renders does. | No. Its code goes to the browser too. |
| Have its HTML made on the server | Yes. | Yes. Then it runs in the browser too. |
Try it below. Each button is a version of the page's app/page.tsx, built with real Next.js. Before you click, guess which versions build.
app/page.tsxNext.jsapp/page.tsximport { getTeas } from "./teas";import TeaCard from "./tea-card"; export default async function Home() { const teas = await getTeas(); 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 ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentHome is an async function, and line 5 waits for getTeas(). A Server Component can do that. The build passes and prints ○ /, prerendered as static content: here, made during the build.
The first and last versions build; the middle two don't. A Server Component can wait for data, but it can't keep state. It can hand a Client Component data, but not an ordinary function: a function is code, and a Server Component's code stays on the server. A Server Function is the way through. In the last version, addToCart comes from app/actions.ts, whose line 1 is "use server". Open its Network tab: the page's JavaScript holds the button's code, but not the text addToCart ran on the server from line 4 of app/actions.ts. The function stays on the server, and the button calls it from there.
Server Components and Server Functions teach how React itself does this. Next.js has a whole stage on it, Server and Client Components.
Forms and actions
In React 19, a form's action can be a function. When the form is submitted, React calls it with the form's fields as FormData. When the action finishes without an error, React resets the form's uncontrolled fields: the ones without a value prop, like this box. A controlled field, whose value comes from state, keeps what its state says. useActionState keeps what the action returned, and says whether it's still running. Type Fresh in the box and press Save review.
import { useActionState } from "react";import { saveReview } from "./api.js"; export default function App() { const [message, formAction, isPending] = useActionState(saveReview, ""); return ( <form action={formAction}> <label> Your review <input name="review" /> </label> <button disabled={isPending}>{isPending ? "Saving…" : "Save review"}</button> {message && <p>{message}</p>} </form> );}Line 5 calls useActionState with the action, saveReview, and a first message, "". It gives back three things: the latest message, a formAction to give the form on line 7, and isPending. While saveReview waits (600 ms, in the api.js tab), line 11 shows Saving… on a button that can't be pressed. Then line 12 shows Saved: Fresh, and the box is empty again.
What changes in Next.js. The action can be a Server Function, so saving happens on the server. The form is the same code with "use client" on line 1, because useActionState is a hook, and saveReview now comes from app/actions.ts:
app/review-form.tsx"use client"; import { useActionState } from "react";import { saveReview } from "./actions"; export default function ReviewForm() { const [message, formAction, isPending] = useActionState(saveReview, ""); return ( <form action={formAction}> <label> Your review <input name="review" /> </label> <button disabled={isPending}>{isPending ? "Saving…" : "Save review"}</button> {message && <p>{message}</p>} </form> );}GET /200
- Response headers
Varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encodingx-nextjs-cacheHITx-nextjs-prerender1x-nextjs-stale-time300X-Powered-ByNext.jsCache-Controls-maxage=31536000Content-Typetext/html; charset=utf-8
The HTML that arrived, one element per line, with the code of its 16 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/3cfn99dbrcvvp.js">
<script src="/_next/static/chunks/2gsxhio73malr.js" async=""></script>
<script src="/_next/static/chunks/2ki52vdnbo2ft.js" async=""></script>
<script src="/_next/static/chunks/0ooaq-girjaeh.js" async=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
<script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
<script src="/_next/static/chunks/1v8osnl9wukv9.js" async=""></script>
<title>Tea shop</title>
<script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
</head>
<body>
<div hidden="">
<!--$-->
<!--/$-->
</div>
<!--&-->
<main>
<h1>Mint tea</h1>
<form action="" enctype="multipart/form-data" method="POST"><input type="hidden" name="$ACTION_REF_1"><input type="hidden" name="$ACTION_1:0" value="{"id":"60bb3a4b6c673488ffcfb081cc3359ac63e6b36614","bound":"$@1"}"><input type="hidden" name="$ACTION_1:1" value="[""]"><input type="hidden" name="$ACTION_KEY" value="k67721075b722be81e4508015683e29d2"><label>Your review <input name="review"></label><button>Save review</button></form>
</main>
<!--$-->
<!--/$-->
<!--/&-->
<script id="_R_">…</script>
<script src="/_next/static/chunks/3cfn99dbrcvvp.js" async=""></script>
<script>…</script>
<script>…</script>
<script>…</script>
<script>…</script>
<script>…</script>
<script>…</script>
<script>…</script>
</body>
</html>The Network tab shows the form's HTML: <form action="" method="POST">, with hidden fields that hold the action's id. So the form works even before the JavaScript has loaded: the browser posts the fields and the id to the page's own address, and the server runs saveReview. Once React has hydrated the page, it sends the form itself, and isPending shows Saving… as in the live example. The page's JavaScript contains Save review, but not saveReview ran on the server from line 5 of app/actions.ts.
saveReview, and the answer comes back to useActionState, which shows it.Forms, Form actions and Optimistic UI teach these in full. The course's data stage builds forms that save with Server Functions.
A tea page that uses all of it
Real pages mix all of these. Here is the Mint tea page: the card from the overview, the reviews, which are read for each visitor, and the review form. Read app/page.tsx, then the other files.
app/page.tsximport { Suspense } from "react";import { getTea } from "./data";import TeaCard from "./tea-card";import Reviews from "./reviews";import ReviewForm from "./review-form"; export default async function TeaPage() { const tea = await getTea("mint"); return ( <main> <TeaCard name={tea.name} price={tea.price} /> <h2>Reviews</h2> <Suspense fallback={<p>Loading reviews…</p>}> <Reviews teaId={tea.id} /> </Suspense> <ReviewForm /> </main> );}$3 a cup
- Asha: Fresh and bright.
- Ravi: Lovely after dinner.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
- Lines 7 and 8 of
app/page.tsx:TeaPageis anasyncServer Component that waits for the tea. - Line 11 hands the tea's
nameandpricetoTeaCard, the Client Component with the count. - Lines 13 to 15 put
Reviewsin aSuspense.Reviewsis anasyncServer Component too, and line 9 ofapp/data.tsmakes it wait for a request, so the server streams it in. - Line 16 adds
ReviewForm, a Client Component whose action is the Server FunctionsaveReview.
The Terminal says ◐ /: a static shell, with the reviews streamed in for each request. In the Network tab, the HTML holds Loading reviews… first, and the reviews from Asha and Ravi further down. The page's JavaScript contains Add to cart and Save review, but not the reviews' text. Reviews is a Server Component, so its code, and the code that loads the reviews, stay on the server. What it rendered, the reviews' text, is sent to the browser in the HTML.
Sort these components: which ones can stay Server Components, and which need "use client"?
TeaPage, which waits forgetTea("mint")Reviews, which waits for the reviews insideSuspenseTeaCard, whose button counts clicks withuseStateReviewForm, which callsuseActionState- A footer that shows the shop's address
- A line that shows the window's width, measured in an Effect
- A list that reads the shop's database with a secret password
- A Like button that turns red when it's clicked
Sort each component: can it stay a Server Component, or does its file need "use client"?
Common mix-ups
- "Next.js has its own kind of React." No. It installs React 19.3.0, and pages, layouts and buttons are ordinary React components. Next.js decides where each one runs.
- "A Client Component only runs in the browser." No. Its HTML is made on the server too, like each card's
Add to cartin the recorded HTML. Then its code also runs in the browser. - "Every component needs
"use client"." No. Only the ones that need state, event handlers, Effects or browser APIs. The rest stay Server Components, and their code stays on the server. - "Effects run on the server first." No. Effects run only in the browser. The server's HTML shows the state from before the Effect, like
Measuring your window…. - "A Next.js page loads its data in an Effect." It can, in a Client Component, but it usually doesn't need to: an
asyncServer Component waits for the data on the server, and the data is in the HTML. - "A Server Function's code is sent to the browser." No. The browser gets an id it sends back; the code stays on the server.
Each row is one piece of React: how it behaves in this lesson's client-rendered React examples, and what changes in Next.js.
| Piece of React | In this lesson's client-rendered React examples | In Next.js |
|---|---|---|
| Components and props | Every component runs in the browser, because these examples render there. | Components start as Server Components. A file with "use client" starts the part that also runs in the browser. |
| State and event handlers | Anywhere. | Only in Client Components. |
| Effects | Run after React puts the component on the screen. | The same, in the browser only. The server's HTML shows the state from before the Effect. |
| Loading data | In a promise started in the browser, like getTeas() in api.js. | In an async Server Component, with await. |
| Suspense | Shows the fallback while a part waits in the browser. | The same, and the server streams the waiting part into the HTML when it's ready. |
| Form actions | A function that runs in the browser. | Often a Server Function, which runs on the server. The form works before the JavaScript has loaded. |
Practice exercises
The React exercises run in the page: press Edit & run to try your answer. The Next.js ones can't run inside this page.
In the three tea cards (TeaCard.jsx is the card from the section above), you click Mint tea's button twice. What does it say now?
import TeaCard from "./TeaCard.jsx";
const teas = [
{ id: "green", name: "Green tea", price: 4 },
{ id: "mint", name: "Mint tea", price: 3 },
{ id: "lemon", name: "Lemon tea", price: 3 },
];
export default function App() {
return (
<main>
<h1>Tea shop</h1>
{teas.map((tea) => (
<TeaCard key={tea.id} name={tea.name} price={tea.price} />
))}
</main>
);
}In cart: 2. Each TeaCard has its own state, so only Mint tea's count goes up, to 2, and its button shows In cart: ${count}. Green tea's and Lemon tea's still say Add to cart.
These are the Mint tea page's files. Which two start with "use client"?
app/
actions.ts
data.ts
layout.tsx
page.tsx
review-form.tsx
reviews.tsx
tea-card.tsxtea-card.tsx and review-form.tsx. The card keeps a count in state and handles clicks, and the form calls useActionState. page.tsx and reviews.tsx are async Server Components, actions.ts starts with "use server", and data.ts and layout.tsx need no browser.
This cart copies its total into state with an Effect. Rewrite it without the Effect. What does the paragraph say after one click on Add lemon tea?
import { useEffect, useState } from "react"; const prices = { green: 4, mint: 3, lemon: 3 }; export default function App() { const [cart, setCart] = useState(["green", "mint"]); const [total, setTotal] = useState(0); useEffect(() => { setTotal(cart.reduce((sum, id) => sum + prices[id], 0)); }, [cart]); return ( <> <p> {cart.length} teas, total ${total} </p> <button onClick={() => setCart([...cart, "lemon"])}>Add lemon tea</button> </> );}import { useState } from "react"; const prices = { green: 4, mint: 3, lemon: 3 }; export default function App() { const [cart, setCart] = useState(["green", "mint"]); const total = cart.reduce((sum, id) => sum + prices[id], 0); return ( <> <p> {cart.length} teas, total ${total} </p> <button onClick={() => setCart([...cart, "lemon"])}>Add lemon tea</button> </> );}Work the total out while rendering, on line 7. There's no second state to keep in step and no extra render. After one click, the cart has three teas: 4 + 3 + 3 makes 3 teas, total $10.
This page shows nothing at all for 600 ms, because TeaList waits and there's no Suspense around it. Add one with the fallback Loading teas…. What does the page show during the first 600 ms now?
import { use } from "react";import { getTeas } from "./api.js"; const teasPromise = getTeas(); function TeaList({ teasPromise }) { const teas = use(teasPromise); return ( <ul> {teas.map((tea) => ( <li key={tea.id}>{tea.name}</li> ))} </ul> );} export default function App() { return ( <main> <h1>Tea shop</h1> <TeaList teasPromise={teasPromise} /> </main> );}import { Suspense, use } from "react";import { getTeas } from "./api.js"; const teasPromise = getTeas(); function TeaList({ teasPromise }) { const teas = use(teasPromise); return ( <ul> {teas.map((tea) => ( <li key={tea.id}>{tea.name}</li> ))} </ul> );} export default function App() { return ( <main> <h1>Tea shop</h1> <Suspense fallback={<p>Loading teas…</p>}> <TeaList teasPromise={teasPromise} /> </Suspense> </main> );}With <Suspense fallback={<p>Loading teas…</p>}> around TeaList, only the list waits. For the first 600 ms, the page shows Tea shop and Loading teas…, then the three teas.
This is app/page.tsx, a Server Component. AddButton is a Client Component that calls onAdd on a click. Does next build pass, or fail?
import AddButton from "./add-button";
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<AddButton name="Mint tea" onAdd={(name) => console.log(name)} />
</main>
);
}
It fails. A function is code, and a Server Component's code stays on the server, so it can't be a prop of a Client Component. The recorded build stops with: Event handlers cannot be passed to Client Component props. A Server Function, from a file that starts with "use server", would work.
This review form gives no sign that it's saving. Make its button say Saving… and turn off while saveReview waits. What does the button say during the wait?
import { useActionState } from "react";import { saveReview } from "./api.js"; export default function App() { const [message, formAction] = useActionState(saveReview, ""); return ( <form action={formAction}> <label> Your review <input name="review" /> </label> <button>Save review</button> {message && <p>{message}</p>} </form> );}import { useActionState } from "react";import { saveReview } from "./api.js"; export default function App() { const [message, formAction, isPending] = useActionState(saveReview, ""); return ( <form action={formAction}> <label> Your review <input name="review" /> </label> <button disabled={isPending}>{isPending ? "Saving…" : "Save review"}</button> {message && <p>{message}</p>} </form> );}isPending, the third value from useActionState, is true while saveReview waits. The button shows Saving… and can't be pressed again, then shows Save review once the message arrives.
Check your understanding
For each question, ask where the code runs: on the server, in the browser, or both.
Question 1 of 7In a Next.js app, which components can call
useState?Choose an answer to see the explanation.
Question 2 of 7The recorded HTML of the window width page says
Measuring your window…, not a width. Why?Choose an answer to see the explanation.
Question 3 of 7What does this app show during its first 600 ms?
Read the code, then predictimport { Suspense, use } from "react"; import { getTeas } from "./api.js"; const teasPromise = getTeas(); function TeaList({ teasPromise }) { const teas = use(teasPromise); return ( <ul> {teas.map((tea) => ( <li key={tea.id}>{tea.name}</li> ))} </ul> ); } export default function App() { return ( <main> <h1>Tea shop</h1> <Suspense fallback={<p>Loading teas…</p>}> <TeaList teasPromise={teasPromise} /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 4 of 7Which of these can a Server Component pass to a Client Component as a prop?
Choose an answer to see the explanation.
Question 5 of 7Where does a Next.js page usually load the data it shows?
Choose an answer to see the explanation.
Question 6 of 7When the review form's action is a Server Function, what does the browser get for
saveReview?Choose an answer to see the explanation.
Question 7 of 7In this form, what does the button say while
saveReviewwaits?Read the code, then predictimport { useActionState } from "react"; import { saveReview } from "./api.js"; export default function App() { const [message, formAction, isPending] = useActionState(saveReview, ""); return ( <form action={formAction}> <label> Your review <input name="review" /> </label> <button disabled={isPending}>{isPending ? "Saving…" : "Save review"}</button> {message && <p>{message}</p>} </form> ); }Choose an answer to see the explanation.
Key takeaways
- Next.js code is React code: pages, layouts and buttons are components with props, state, Effects and Suspense.
- Components start as Server Components. They can be
asyncand wait for data, and their code stays on the server. - A file that starts with
"use client"marks the part that also runs in the browser. State, event handlers and Effects live there. - Effects run only in the browser. The server's HTML shows the state from before them.
Suspenseshows a fallback while a part waits, and the server streams that part in. A Client Component can read a promise from the server withuse.- A Server Component hands a Client Component data, promises and Server Functions, never ordinary functions. A Server Function runs on the server; the browser only gets a reference to it.
- A form's action can be a Server Function, and the form then works before the JavaScript has loaded.
Remember the one-liner.
Write React as you know it; Next.js runs it on the server, except the parts marked "use client".
Coming next: How Next.js works: a first look. It follows one visit to the tea shop, from next build to a click on a link, and shows what travels between the server and the browser.