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

Client Components

Learn what "use client" really marks in Next.js: where the browser part starts, how Client Components still render on the server, and why it belongs low.

By the end, you can
  • 01
    Place the boundaryTell which files a "use client" line brings into the browser, and put it on the smallest part that needs it.
  • 02
    Explain both rendersSay what a Client Component does on the server during the build, and what it does in the browser when React hydrates it.
  • 03
    Use the browser safelyKeep state, Effects and browser APIs where they work, and wrap a package's component that lacks the directive.

A cup counter that works in the browser

Server Components in Next.js kept everything on the server. Now the tea shop's Mint tea page gets a cup counter: two buttons, and a total that changes as you click. That needs code running in the browser after the page is on the screen, so it needs a Client Component: a component whose code also goes to the browser, where it can keep state and respond to clicks. In Next.js, you start one with "use client" at the top of a file. Why Next.js? used that line for an Add to cart button. That line marks where the client part of the app starts, and everything the file imports goes to the browser with it.

Here is the page, built and run with Next.js 16.4.3. The recorder also opened it in real Chrome and clicked One more twice. The Steps tab is open on the first click.

The Mint tea page with a cup counter, clicked in real ChromeNext.js
app/
app/cup-picker.tsx
"use client"; import { useState } from "react"; export default function CupPicker({ price }: { price: number }) {  const [cups, setCups] = useState(1);  console.log(`CupPicker rendered with ${cups} cup`);   return (    <p>      <button onClick={() => setCups(cups - 1)} disabled={cups === 1}>        One less      </button>{" "}      {cups} {cups === 1 ? "cup" : "cups"}: ${cups * price}{" "}      <button onClick={() => setCups(cups + 1)}>One more</button>    </p>  );}
Recorded from Next.js 16.4.3

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

localhost:3000/Tea shop
Mint tea

Fresh mint leaves, dried in the sun.

One less 2 cups: $6 One more

Focus
the button “One more”
Scrolled
0 px down

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

In app/cup-picker.tsx, line 1 is "use client". Line 6 keeps cups in state, starting at 1, and line 7 prints a message each time CupPicker renders. Lines 11 to 13 are the One less button, disabled at 1 cup. Line 14 shows the count and the total, and line 15 is the One more button. In app/page.tsx, a Server Component, line 8 renders CupPicker with price={3}.

In the Steps tab, the first step opened the page, with one request: the page itself, the document. Each click on One more changed the text, to 2 cups: $6 and then 3 cups: $9, and made no request at all. This click only updates local state, which lives in the browser, and React updated the page there. (A handler that fetches data or calls a Server Function would still send a request.)

Now open the Terminal tab: CupPicker rendered with 1 cup is in next build's output. The Client Component ran on the server too, during the build, and the Browser tab shows the HTML it made: One less 1 cup: $3 One more. (It's printed twice for the reason the last lesson gave: with Cache Components on, the build renders a page twice.) The recording also searched the JavaScript files the page loads: they hold One more, from CupPicker's code, and not Fresh mint leaves, from the page's.

So CupPicker runs in two places. On the server, it renders to HTML. In the browser, React hydrates it: hydration is React running the Client Components again in the browser, matching them to the HTML already on the screen, and attaching their event handlers, so the buttons start to work.

Two windows. Left, the server during next build: a shaded card CupPicker with cups equal to 1, and an arrow down to a card HTML: 1 cup: $3. A curved arrow goes to the right window, the browser: a card HTML: 1 cup: $3, and an arrow down to a shaded card CupPicker with cups equal to 1, where React attaches the handlers.On the server, in next buildserverCupPicker: cups = 1HTML: 1 cup: $3renders it to HTMLIn the browserbrowserHTML: 1 cup: $3CupPicker: cups = 1runs it again: hydration
On a first visit, CupPicker renders in two places. On the server, during next build, it makes the HTML. In the browser, React runs it again, matches the HTML, and attaches its onClick handlers. Shaded cards are code that goes to the browser.
Real-life analogyA hands-on room in a museum

In a science museum, most exhibits are behind glass. One door leads to a hands-on room, and everything past that door can be touched, even the exhibits without a sign of their own. Staff set up the room before the museum opens, and switch the machines on when the first visitors walk in.

In real life: Exhibits behind glass: you look, but you can't touch
In Next.js: Server Components: the browser gets what they rendered
In real life: The door marked "hands-on room"
In Next.js: A file that starts with "use client"
In real life: Everything past that door, even exhibits with no sign of their own
In Next.js: Everything that file imports: it all goes to the browser
In real life: Machines set up before opening, switched on when visitors arrive
In Next.js: HTML rendered on the server, then hydrated in the browser
The main idea

A file that starts with "use client" is where the client part of your app starts. Its components, and everything the file imports, also go to the browser. A Client Component still renders on the server first, into the HTML. Then React hydrates it in the browser, where it keeps state, runs Effects and handles events.

"use client" marks an entry point

Next.js's docs call a file with "use client" an entry point: the place where the client part of the app is entered from the server part. The line between the two parts is called the boundary. Two rules decide what's on the client side of it:

  • The directive goes at the very top of the file, before the imports, and it applies to the whole file. A component can't be marked on its own.
  • Every file an entry point imports is part of the client too, with or without the directive, and so is everything those files import.

So you don't put "use client" in every file of the client part, only in the files that Server Components render. Here, OrderBox is the entry point. It imports CupCounter from a file without the directive, which imports formatPrice from price.ts. The page imports price.ts too.

app/order-box.tsx: the entry pointTSX
"use client";

import { useState } from "react";
import CupCounter from "./cup-counter";

export default function OrderBox({ price }: { price: number }) {
  const [added, setAdded] = useState(false);

  return (
    <section>
      <CupCounter price={price} />
      <button onClick={() => setAdded(true)}>{added ? "Added" : "Add to cart"}</button>
    </section>
  );
}
app/cup-counter.tsx: no directiveTSX
import { useState } from "react";
import { formatPrice } from "./price";

export default function CupCounter({ price }: { price: number }) {
  const [cups, setCups] = useState(1);

  return (
    <p>
      {cups} {cups === 1 ? "cup" : "cups"}, {formatPrice(cups * price)}{" "}
      <button onClick={() => setCups(cups + 1)}>One more</button>
    </p>
  );
}

In order-box.tsx, line 1 is the directive, line 4 imports CupCounter, and line 11 renders it. In cup-counter.tsx, line 1 imports useState with no directive anywhere in the file, and line 10 is the One more button. Try three versions of who imports CupCounter. Before you click, guess which one fails.

Playground: who imports CupCounter?
The Mint tea page, with this version of its filesNext.js
app/
app/order-box.tsx
"use client"; import { useState } from "react";import CupCounter from "./cup-counter"; export default function OrderBox({ price }: { price: number }) {  const [added, setAdded] = useState(false);   return (    <section>      <CupCounter price={price} />      <button onClick={() => setAdded(true)}>{added ? "Added" : "Add to cart"}</button>    </section>  );}
Recorded from Next.js 16.4.3
$ 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 content
Try it yourself
OrderBox imports it

Line 1 makes order-box.tsx an entry point, and line 4 imports CupCounter. cup-counter.tsx has no directive, but it's part of the client now. The build prints ○ /, and the page's JavaScript holds One more, from CupCounter.

Every version was built with next build from Next.js 16.4.3; the Terminal shows what it printed. cup-counter.tsx is the same file in the first two versions. Reset goes back to the first version.

In the first version, the recording searched the page's JavaScript. It holds One more, which is only in cup-counter.tsx, and Free today, which is only in price.ts: a text the page never shows, because the price is never 0. Neither file has the directive; they went to the browser because order-box.tsx imports them. The page's own text, Fresh mint leaves, isn't there.

In the second version, the page imports cup-counter.tsx itself. It's the same file, but now a Server Component imports it, so it's part of the server, and its useState stops the build. The import trace under the error shows the path: Server Component: ./app/cup-counter.tsx ./app/page.tsx. In the third version, cup-counter.tsx has its own directive, so it's an entry point too, and the page can render it.

Four files in a chain of imports: page.tsx imports order-box.tsx, which imports cup-counter.tsx, which imports price.ts. page.tsx is a Server Component. order-box.tsx is shaded, with use client on line 1: the entry point. cup-counter.tsx and price.ts are shaded too, without the directive, because they are imported from the client part. The page imports price.ts as well, so it gets one copy for the server and one for the browser.page.tsxa Server Component: no directiveorder-box.tsx"use client" on line 1: the entry pointcup-counter.tsxno directive: client, because it's importedprice.tsthe page imports it too: one copy for each
Shaded files send their code to the browser. order-box.tsx starts with "use client", so the client part starts there, and cup-counter.tsx and price.ts come along because it imports them. The page imports price.ts too, so Next.js also builds a copy of it for the server.

A file can be on both sides. The page and CupCounter both import price.ts, so Next.js builds it once for the server and once for the browser. Only props cross the boundary as data: the page gives OrderBox price={3}, and like every prop from a Server Component, it travels in the RSC payload: React's own description of what the Server Components rendered, with the props they pass to Client Components. Which values can cross, and how to put Server Components inside Client Components, is the next lesson of this stage.

State, Effects and event handlers

Past the boundary, you have all of React: state, event handlers, Effects, refs, context, custom hooks, and the browser's own APIs, like window and localStorage. CupPicker used state and event handlers. An Effect runs after React has put the component on the screen, and only in the browser, which makes it the place to read data that only the browser has.

Here the Mint tea page gets a Save for later button that remembers the choice in localStorage: a small store the browser keeps for each site, which survives a reload. The recorder opened the page, clicked the button, then opened the page again. The Steps tab is open on the last step.

A Save for later button that reads localStorage in an EffectNext.js
app/
app/save-button.tsx
"use client"; import { useEffect, useState } from "react"; export default function SaveButton({ id }: { id: string }) {  const [saved, setSaved] = useState(false);   useEffect(() => {    setSaved(localStorage.getItem("saved-tea") === id);  }, [id]);   function save() {    localStorage.setItem("saved-tea", id);    setSaved(true);  }   return (    <button onClick={save} disabled={saved}>      {saved ? "Saved" : "Save for later"}    </button>  );}
Recorded from Next.js 16.4.3

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

localhost:3000/Tea shop
Mint tea

Fresh mint leaves, dried in the sun.

Saved
Focus
nothing: the page itself
Scrolled
0 px down

Requests the page made during this step (1), without the files it loaded

  1. GET/200a whole page

In app/save-button.tsx, line 6 starts saved at false. Lines 8 to 10 are an Effect: once the button is on the screen, it reads localStorage and sets saved. Lines 12 to 15 are save, which the onClick on line 18 calls: it stores Mint tea's id and sets saved to true. Line 19 shows Saved or Save for later.

In the Steps tab, the first step shows Save for later. The click changed it to Saved and made no request. The last step opened the page again: a whole new page load, one document request, and the button says Saved. The server didn't know about the saved tea. Its HTML, in the Network tab, says Save for later, the same for every visitor. The Effect changed it, in the browser, after hydration.

Client Components still render on the server

"Client" doesn't mean "only in the client". Next.js's docs say a Client Component runs on the server and in the browser. On a first visit, it renders on the server into the HTML, during the build or for the request. Then React renders it again in the browser while it hydrates. The docs add that after a client-side navigation, like a click on a Link, the router gets only the RSC payload, and Client Components render in the browser alone. Step through the first visit to the Mint tea page, then the click:

Playground: where CupPicker runs, step by step
Who does whatOpening the page
What the visitor seesafter step 1
tea-shop.example/

A blank page

React isn't running yet: clicks do nothing.

The page isn't complete yet.

Step 1 of 5
Next.js: next build renders the page

Before any visitor comes, next build runs MintTeaPage and CupPicker on the server, with cups at 1, and keeps the HTML. CupPicker rendered with 1 cup is in the build's output.

Each step is one thing that happens, in order. The screens are from the recording above: the HTML next start sent, and the page in real Chrome after each click. Reset goes back to the first step of Opening the page.

After step 3 of Opening the page, the visitor sees the buttons, but clicking them does nothing until step 5. React runs only CupPicker in the browser: MintTeaPage is a Server Component, and it never hydrates. Each row below is one piece of CupPicker or SaveButton: what it does during the server render, and what it does in the browser.

One component, two renders
In the componentOn the serverIn the browser
useState(1)Starts at 1, and stays there: the HTML says 1 cup.Starts at 1 again, then changes with each click.
onClick={…}Never called. The HTML has a plain <button>.Attached when React hydrates the component.
useEffect(…)Never runs.Runs after hydration, and again when its dependencies change.
localStorage during renderNot defined: the build stops.Defined, but the first render must still match the HTML.
console.log during renderPrinted in next build's output.Printed in the browser's console.

So the code that runs while a Client Component renders must work in both places, and give the same result in both. Try three places for SaveButton to read localStorage. Before you click, guess which one the build stops.

Playground: where should SaveButton read localStorage?
The Mint tea page, with this version of app/save-button.tsxNext.js
app/
app/save-button.tsx
"use client"; import { useEffect, useState } from "react"; export default function SaveButton({ id }: { id: string }) {  const [saved, setSaved] = useState(false);   useEffect(() => {    setSaved(localStorage.getItem("saved-tea") === id);  }, [id]);   function save() {    localStorage.setItem("saved-tea", id);    setSaved(true);  }   return (    <button onClick={save} disabled={saved}>      {saved ? "Saved" : "Save for later"}    </button>  );}
Recorded from Next.js 16.4.3
$ 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 content
Try it yourself
In an Effect

Line 6 starts saved at false, on the server and in the browser alike. After hydration, the Effect on lines 8 to 10 reads localStorage. Open the Steps tab: after a reload, the button says Saved, with no errors.

Every version was built with next build from Next.js 16.4.3, and the two that built were opened in real Chrome: open the page, click Save for later, open the page again. The Steps tab shows each step and any error the console logged. Reset goes back to the first version.

The Effect version works, as above. The second version reads localStorage on line 6, while the component renders. The build renders it on the server, where localStorage doesn't exist, and stops with ReferenceError: localStorage is not defined. The third version skips it on the server with typeof window !== "undefined", so the build passes. But a visitor who saved Mint tea gets HTML that says Save for later, and the browser's first render says Saved. In its Steps tab, the last step logged Minified React error #418. That's the short form React uses in production for "Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client." React threw away that part of the HTML and rendered it again in the browser. The Effect version avoids the problem: its first render matches the HTML, and the Effect changes it afterwards. (Hydration, in the React course, shows what React checks.)

Two rows, three columns: the HTML, the first render in the browser, and then. Top row, read in an Effect: the HTML says Save for later, the first render in the browser says Save for later, the same as the HTML, and then the Effect changes it to Saved. Bottom row, read during render: the HTML says Save for later, the first render says Saved, which differs from the HTML, and React logs error 418 and renders it again.The HTML saysFirst render in the browserThenSave for laterSave for laterSavedread in an Effectsame as the HTMLthe Effect runsSave for laterSavederror #418read during renderdiffers from the HTMLReact renders it again
For a visitor who saved Mint tea before. Top: SaveButton reads localStorage in an Effect, so its first render in the browser matches the HTML, and the Effect changes it afterwards. Bottom: it reads it during render, behind typeof window, so the first render doesn't match, and React logs error #418.

Keep the boundary low

Every "use client" file pulls its imports into the browser, so the higher in the component tree you put it, the more goes with it. Put it on the small parts that need the browser, the leaves of the tree, and keep the rest on the server. Here is a Green tea page two ways: the whole page as a Client Component, or only its Add to cart button.

Playground: where should the boundary go?
The Green tea page, with this version of app/page.tsxNext.js
app/
app/page.tsx
"use client"; import { useState } from "react";import { teas } from "./data"; export default function GreenTeaPage() {  const tea = teas.find((item) => item.id === "green")!;  const [added, setAdded] = useState(false);   return (    <main>      <h1>{tea.name}</h1>      <p>{tea.about}</p>      <button onClick={() => setAdded(true)}>{added ? "Added" : "Add to cart"}</button>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/Tea shop
Green tea

Small leaves picked in early spring, steamed the same day.

Add to cart

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

Try it yourself
The whole page

Line 1 makes the page itself a Client Component, so app/data.ts, imported on line 4, goes to the browser too: all three teas' descriptions, though the page shows one.

Both versions were built and run with Next.js 16.4.3. Their pages look the same; the table below shows what their JavaScript holds. Reset goes back to the first version.

Both pages look the same in the Browser tab, and both were prerendered as ○ /, with the same content in their HTML. The difference is in the JavaScript the browser downloads, which the page doesn't show. The recording searched each version's JavaScript for three texts. Each row is one text: is it in that version's JavaScript?

What each version's JavaScript holds
The textThe whole pageOnly the button
picked in early spring: Green tea's description, which the page showsYes.No.
dried lemon peel: Lemon tea's description, which the page never showsYes.No.
Add to cart: The button's labelYes.Yes.

With the whole page as a Client Component, data.ts went to the browser, with Lemon tea's description that this page never shows. With only the button, neither description is in the JavaScript; the one the page shows arrives only as rendered text. Each row below compares the two versions:

The whole page, or only the button
QuestionThe whole page is a Client ComponentOnly the button is
What goes to the browser as code?The page, and everything it imports: all of data.ts.Only AddToCart, and what its file imports.
Can the page be async and use secrets?It can't be async, and it must not read secrets: its code goes to the browser, and its server render can put what it reads into the HTML.Yes. It's still a Server Component.
What does React hydrate?The whole page.Only the button.
What does the visitor see first?The same page: Next.js renders both on the server.The same page.
Two halves. Left, the whole page is a Client Component: page.tsx and the data.ts it imports are both shaded, so both go to the browser. Right, only the button is: page.tsx and data.ts are plain, and only add-to-cart.tsx is shaded.The whole pagepage.tsxdata.ts: 3 teasboth go to the browserOnly the buttonpage.tsxdata.tsadd-to-cart.tsxonly the button's code goes
Shaded files send their code to the browser. Left: page.tsx starts with "use client", so data.ts goes too, with all three teas' descriptions. Right: only add-to-cart.tsx does, and the description reaches the browser as one rendered paragraph.

Components from packages without the directive

Components from npm packages follow the same rule. A package whose components need the browser should start their files with "use client", and Next.js's own Link does: you render it from Server Components all the time. Some packages don't, often because they were written before Server Components existed.

Here, acme-rating stands in for one: a small package written for this lesson, placed in the project's node_modules folder the way npm installs one. Its index.js uses useState and has no directive. Packages ship plain JavaScript, so it calls createElement instead of using JSX. Try rendering it from the page, then through a wrapper:

Playground: using a package's Rating
The Mint tea page, with the acme-rating packageNext.js
app/node_modules/acme-rating/
app/page.tsx
import { Rating } from "acme-rating"; export default function MintTeaPage() {  return (    <main>      <h1>Mint tea</h1>      <Rating label="Your rating" />    </main>  );}
next build failed · Next.js 16.4.3
$ 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 ...Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-errorTypeError: (0 , c.useState) is not a function or its return value is not iterable    at ignore-listed frames {  digest: '3028760187'}Export encountered an error on /page: /, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null
Try it yourself
Rendered by the page

The page, a Server Component, renders Rating from the package. The package's file has no directive, so it runs as part of a Server Component, with the build of React made for Server Components, which has no useState. The prerender stops with a TypeError.

Both versions were built with next build from Next.js 16.4.3, and the one that built was opened in real Chrome. The package's files are the same in both. Reset goes back to the first version.

Rendered by the page, the package's code runs as part of a Server Component, with the build of React made for Server Components, which has no useState. The prerender stops with TypeError: (0 , c.useState) is not a function or its return value is not iterable. That's less clear than the message for your own files, which Next.js checks as it compiles them; this package's file got no such message. The fix is a wrapper of your own: app/rating.tsx has "use client" on line 1, imports Rating on line 3 and exports it again on line 5. Now your file is the entry point, and the package comes in as one of its imports. In the wrapper version's Steps tab, clicking 2 stars changes the rating to 2 of 3.

If you write a package

Put "use client" at the top of the files whose components need the browser, so people can render them from Server Components without a wrapper. Next.js's docs warn that some bundlers remove directives when they build a package, so check that yours keeps them.

Where does "use client" go?

Put "use client" on the files that a Server Component renders and that need the browser. The files they import don't need it. Sort these files from this lesson:

Which files start with "use client"?
  • app/page.tsx, which shows the tea and renders OrderBox.
  • order-box.tsx, which the page renders and which keeps added in state.
  • cup-counter.tsx, which only order-box.tsx imports.
  • price.ts, a helper that formats prices.
  • rating.tsx, which wraps a package's Rating for the page.
  • save-button.tsx, which the page renders and which reads localStorage in an Effect.
Try it yourself
0 of 6 correct

Sort each file: does it need the directive at its top, or not?

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

Every card matches a recording in this lesson. The page built without a directive, cup-counter.tsx worked without one while order-box.tsx imported it, and the wrapper and the Save for later button started the client part where the page rendered them.

Common mix-ups

  • "A Client Component renders only in the browser." No. It renders on the server first, into the HTML: CupPicker's message is in next build's output, and its buttons are in the page.
  • "Every file in the client part needs "use client"." No. Only the entry points that Server Components render. cup-counter.tsx had none.
  • "A file without the directive stays on the server." No. If a client file imports it, it goes to the browser: Free today, from price.ts, was in the page's JavaScript.
  • "Making the whole page a Client Component costs nothing." No. Everything it imports goes to the browser, like all three teas' descriptions. The page can no longer be async, and it must not read secrets: its code goes to the browser, and its render on the server can put what it reads into the HTML.
  • "Every click in a Client Component sends a request." No. A click that only updates local state, like One more, sent none. A handler sends one only when it fetches something or calls a Server Function.
  • "A typeof window check makes browser-only code safe while rendering." No. The server and the browser then render different things, and React logs a hydration error and renders that part again.

Practice exercises

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

Exercise 1 · Warm-upIs CupCounter's code sent?

This is app/cup-counter.tsx, which only order-box.tsx imports. Is its code in the JavaScript the page loads?

Starter codeTSX
import { useState } from "react";
import { formatPrice } from "./price";

export default function CupCounter({ price }: { price: number }) {
  const [cups, setCups] = useState(1);

  return (
    <p>
      {cups} {cups === 1 ? "cup" : "cups"}, {formatPrice(cups * price)}{" "}
      <button onClick={() => setCups(cups + 1)}>One more</button>
    </p>
  );
}

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

    Exercise 2 · Warm-upCount the requests

    After hydration, the visitor clicks One more. How many requests does the click send to the server?

    Starter codeTSX
    "use client";
    
    import { useState } from "react";
    
    export default function CupPicker({ price }: { price: number }) {
      const [cups, setCups] = useState(1);
      console.log(`CupPicker rendered with ${cups} cup`);
    
      return (
        <p>
          <button onClick={() => setCups(cups - 1)} disabled={cups === 1}>
            One less
          </button>{" "}
          {cups} {cups === 1 ? "cup" : "cups"}: ${cups * price}{" "}
          <button onClick={() => setCups(cups + 1)}>One more</button>
        </p>
      );
    }
    

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

      Exercise 3 · PracticeFind the line that stops the build

      The build stops with ReferenceError: localStorage is not defined. Which line causes it?

      Starter codeTSX
      "use client";
      
      import { useState } from "react";
      
      export default function SaveButton({ id }: { id: string }) {
        const [saved, setSaved] = useState(localStorage.getItem("saved-tea") === id);
      
        function save() {
          localStorage.setItem("saved-tea", id);
          setSaved(true);
        }
      
        return (
          <button onClick={save} disabled={saved}>
            {saved ? "Saved" : "Save for later"}
          </button>
        );
      }
      

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

        Exercise 4 · PracticeWhat the HTML says

        A visitor saved Mint tea yesterday and opens the page again. What does the button say in the HTML that next start sends?

        Starter codeTSX
        "use client";
        
        import { useEffect, useState } from "react";
        
        export default function SaveButton({ id }: { id: string }) {
          const [saved, setSaved] = useState(false);
        
          useEffect(() => {
            setSaved(localStorage.getItem("saved-tea") === id);
          }, [id]);
        
          function save() {
            localStorage.setItem("saved-tea", id);
            setSaved(true);
          }
        
          return (
            <button onClick={save} disabled={saved}>
              {saved ? "Saved" : "Save for later"}
            </button>
          );
        }
        

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

          Exercise 5 · ChallengeFind the misplaced line

          This file was meant to be a Client Component, but the build fails. Which line is in the wrong place?

          Starter codeTSX
          import { useState } from "react";
          "use client";
          
          export default function CupPicker({ price }: { price: number }) {
            const [cups, setCups] = useState(1);
            return (
              <button onClick={() => setCups(cups + 1)}>
                {cups} {cups === 1 ? "cup" : "cups"}: ${cups * price}
              </button>
            );
          }
          

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

            Exercise 6 · ChallengeWrap a package

            This page renders Rating from acme-rating, whose file has no directive, and the prerender stops with a TypeError. Write app/rating.tsx, a wrapper the page can render. What is its first line?

            Starter codeTSX
            import { Rating } from "acme-rating";
            
            export default function MintTeaPage() {
              return (
                <main>
                  <h1>Mint tea</h1>
                  <Rating label="Your rating" />
                </main>
              );
            }
            

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

              Check your understanding

              For each question, ask where the boundary is, and where each render happens.

              The Client Components quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does "use client" at the top of a file mark?

                Choose an answer to see the explanation.

              2. Question 2 of 7When does CupPicker, a Client Component, render on a first visit to the prerendered page?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does next build do with this page, given the cup-counter.tsx from the lesson, which has no directive?

                Read the code, then predictTSX
                import CupCounter from "./cup-counter";
                import { formatPrice } from "./price";
                
                export default function MintTeaPage() {
                  return (
                    <main>
                      <h1>Mint tea</h1>
                      <p>{formatPrice(3)} a cup. Fresh mint leaves, dried in the sun.</p>
                      <CupCounter price={3} />
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7After hydration, the visitor clicks One more. What does the browser send to the server?

                Choose an answer to see the explanation.

              5. Question 5 of 7SaveButton reads localStorage while it renders, behind typeof window !== "undefined". A visitor who saved Mint tea opens the page again. What happens?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why put "use client" on the Add to cart button rather than on the whole tea page?

                Choose an answer to see the explanation.

              7. Question 7 of 7A package's Rating uses useState, but its file has no "use client". How do you render it from a Server Component page?

                Choose an answer to see the explanation.

              Key takeaways

              • "use client" at the top of a file marks an entry point: that file, and everything it imports, go to the browser, with or without the directive.
              • Put it on the files that Server Components render, not on every file. A file that a Server Component imports is part of the server.
              • Client Components can use state, Effects, event handlers and browser APIs. A state update in the browser sends nothing to the server.
              • They still render on the server first, into the HTML, and React hydrates them in the browser. Code that runs during render must work on the server and give the same result there; read browser-only data in an Effect.
              • Keep the boundary low: make the small interactive parts Client Components, and keep pages, layouts and data on the server.
              • Wrap a package's component that lacks the directive in a file of your own that starts with "use client".

              Remember the one-liner.
              "use client" marks where the browser part starts; it still renders on the server, so keep it small and at the leaves.

              Read it in Next.js's own code

              These links point at Next.js v16.4.3, the version every example was built with. The message for a directive below an import is on lines 318 to 321 of react_server_components.rs, the code that reads directives while Next.js compiles your files. And Next.js's own Link starts with 'use client' on line 1 of link.tsx. 'use client' and 'use server' under the hood reads how the boundary is built.

              Coming next: Composing Server and Client Components. It puts Server Components inside Client Components, and shows which props can cross the boundary.

              CompleteFrontend Clear concepts. Working examples.