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

Fetching in the browser

Fetch data in the browser only when it belongs there: live updates, load-more lists and after-load data, with SWR or TanStack Query and Route Handlers.

By the end, you can
  • 01
    Choose where data loadsTell data that belongs in the browser (live updates, load-more lists, after-load data) from data the server should load.
  • 02
    Fetch with SWR or TanStack QueryUse a key, a fetcher and a refresh interval in a Client Component, and read the requests they make in the network list.
  • 03
    Give the browser an addressWrite a Route Handler for the browser to ask, keep it from being frozen at build time, and start its data on the server.

An order page that checks itself

A visitor orders a mint tea at the tea shop and opens the page for order 12. The kitchen is still brewing it. The page should change to Ready for pickup when the tea is ready, without the visitor pressing reload. The server can't do that alone: it sends the page once. So here the browser asks for the order's status itself, and asks again every 2 seconds.

This example is a real Next.js 16.4.3 project. It was built with next build, run with next start, and opened in real Chrome, which recorded every request the page made. Click a file in the tree to read it.

An order page whose status the browser keeps checkingNext.js
app/api/order/
app/order-status.tsx
"use client"; import useSWR from "swr";import { fetcher } from "./fetcher"; type Order = { id: number; tea: string; status: string }; export default function OrderStatus() {  const { data, error } = useSWR<Order>("/api/order", fetcher, { refreshInterval: 2000 });  if (error) return <p>Could not check your order.</p>;  if (!data) return <p>Checking your order…</p>;  return (    <p>      {data.tea}: {data.status}    </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
Order 12

Mint tea: Brewing

Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/200a whole page
  2. GET/api/order200application/json

app/order-status.tsx is a Client Component: line 1 says "use client", because it runs in the browser after the page loads. Line 9 is the whole idea. useSWR, a hook from a library called SWR, asks for the data under the key "/api/order". It calls fetcher from app/fetcher.ts with that key, and fetcher sends a fetch request to that address. refreshInterval: 2000 tells SWR to ask again every 2,000 milliseconds. Until the first answer arrives, data is empty, and line 11 shows Checking your order….

The address /api/order is a Route Handler: app/api/order/route.ts, a file that answers requests with data instead of a page. Its GET function calls getOrder() from app/orders.ts, a stand-in for the kitchen. The stand-in counts the checks, and the pretend order is ready on the fourth.

The Steps tab is open on step 1: Chrome opened / and waited until the page showed Brewing. It made two requests. The first is the page itself, a document. The second is a fetch from the page's JavaScript: GET /api/order, answered 200 with application/json. Pick step 2, which waited for Ready for pickup: three more GET /api/order, and no new page. The status changed in place. Then open the Network tab: the HTML of / says Checking your order…, not Brewing. The status was never in the HTML. The browser fetched it after the page loaded.

A browser window shows Order 12 and Mint tea: Brewing. An arrow labeled GET /api/order goes to a server window, where the GET function in route.ts calls getOrder(). An arrow labeled JSON comes back. A note says: again every 2 seconds.browserOrder 12Mint tea: BrewingserverGET in route.tsgetOrder()GET /api/orderJSONagain every 2 seconds
The page's HTML arrives once. After that, the browser asks /api/order for itself, and the Route Handler on the server answers with JSON. SWR repeats the question every 2 seconds, so the status changes without a new page.
Real-life analogyA menu on the wall, and a screen of order numbers

A café paints its menu on the wall once: it's the same for everyone, all day. Your order's progress is different. It changes while you wait, so a screen by the counter checks with the kitchen every few seconds and shows the news. The café needs both, and each one is the right tool for its own kind of information.

In real life: The menu painted on the wall, the same for every customer
In Next.js: HTML made on the server, once
In real life: The screen by the counter that shows each order's progress
In Next.js: A Client Component that keeps asking
In real life: The screen checking with the kitchen every few seconds
In Next.js: SWR's refreshInterval: 2000
In real life: The kitchen's window where the screen gets its news
In Next.js: The Route Handler at /api/order
The main idea

Load data on the server by default. Fetch it in the browser when it has to change after the page has loaded: while the page is open, after the visitor does something, or only for this visitor once they ask. A Client Component does the fetching, usually with a library like SWR or TanStack Query, and asks one of your app's Route Handlers.

When the browser should ask

In Next.js, the first answer for data is the server. A Server Component reads it while it renders, so it's in the HTML and the browser sends no request for it, as Fetching data in Server Components showed. When a Client Component needs server data once, the server can start it and pass the promise down, as Composing Server and Client Components showed with the reviews.

The browser should ask in three cases:

  • The data changes while the page is open. An order's status, the tins left in stock, new messages. The server sent the page once; only the browser can ask again later.
  • The data depends on what the visitor does after the page loads. The letters they type in a search box, or a click on Load more at the end of a list. An infinite list is a list that loads its next part when the visitor asks or scrolls to the end.
  • Only this visitor needs it, and only later. Their past orders, in a panel they may never open. The page itself stays the same for everyone, and the visitor's part is asked for when the panel opens.

Each row is one kind of data on a tea page, and where it should load:

Where each kind of data loads
The dataWhere and when it loadsIn short
The tea's name, price and descriptionOn the server, in a Server Component. It's the same for everyone and belongs in the first HTML.Server
Reviews a Client Component shows once, with a Show all buttonOn the server. Pass the promise to the Client Component and read it with use().Server, then a promise
An order's status, which changes while the page is openIn the browser, asked again every few seconds.Browser
More teas, after the visitor presses Load moreIn the browser, one page at a time, when the visitor asks.Browser
Teas that match what the visitor is typingIn the browser, after each change to the search box.Browser
The signed-in visitor's past orders, in a panel they open laterIn the browser, when the panel opens. Nobody needs it in the first HTML.Browser
Why a library, and not an Effect?

You can fetch in an Effect, and Composing Server and Client Components did once. But real pages need more than one request: a copy to show while the next request runs, one request for two components that want the same data, asking again on a timer, and ignoring an old answer that arrives after a newer one. A data library does all of that. Data libraries, in the React course, builds a small one to show how. Next.js's guide names SWR and TanStack Query for Client Components that need a shared copy in the browser.

SWR: one key, one shared copy

SWR is a data library from Vercel, the company behind Next.js, and its name comes from stale-while-revalidate: show the copy you have, which may be old (stale), while you ask for a fresh one (revalidate). The examples use version 2.5.1, the latest on October 30, 2026. Here is the order status again, line by line:

  • Line 3 imports useSWR. It's a React hook, so it only works in a Client Component.
  • Line 9 calls it with three things: the key "/api/order", the fetcher, and options. SWR keeps one copy of the data for each key. When it has no copy yet, or the copy is old, it calls fetcher("/api/order"). <Order> tells TypeScript the shape of the data.
  • Lines 2 to 5 of app/fetcher.ts are the fetcher: line 3 sends the request, line 4 throws if the answer isn't OK, and line 5 reads the JSON. SWR doesn't send requests itself; it calls whatever fetcher you give it.
  • Lines 10 and 11 handle the two states before there's a status: error if the fetcher threw, and no data while the first request runs.

Line 11 also explains the HTML. next build prerendered the page, ○ /, and while it rendered OrderStatus on the server, SWR had no data and sent nothing. So the HTML holds Checking your order…, and the first request waits for the page's JavaScript.

One key means one copy, shared by every component that reads it. Here the shop adds a badge to the layout, so every page shows the order's status in its header. The badge reads the same key:

A badge in the layout that reads the same keyNext.js
app/api/order/
app/order-badge.tsx
"use client"; import useSWR from "swr";import { fetcher } from "./fetcher"; export default function OrderBadge() {  const { data } = useSWR<{ status: string }>("/api/order", fetcher);  return <p>Your order: {data ? data.status : "…"}</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

Your order: Brewing

Order 12

Mint tea: Brewing

Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/200a whole page
  2. GET/api/order200application/json

Line 7 of app/order-badge.tsx asks for "/api/order" with no refreshInterval. Line 10 of app/layout.tsx puts the badge in a <header>. The Steps tab shows step 1: two components want the order, and Chrome sent one GET /api/order. SWR saw the same key twice and shared the request. In step 2, the three requests came from OrderStatus's timer, yet the badge says Your order: Ready for pickup too, because both components show the same copy.

Two components, OrderBadge and OrderStatus, each point at a box labeled SWR that holds one copy, the key /api/order. One arrow goes from the box to a tag: GET /api/order, one request.OrderBadgeOrderStatus/api/orderSWRGET/api/orderone request
OrderBadge and OrderStatus ask SWR for the same key, /api/order. SWR keeps one copy for that key, so the page sends one request, and both components show its answer.

SWR asks again at other moments too. Its docs list them: when a component that reads the key mounts, when the window gets focus again, when the network comes back, and on a timer you set.

TanStack Query and a list that loads more

TanStack Query is the other library Next.js's guide shows. It does the same job with more options, and it needs a provider: a component that holds its QueryClient, the object that stores every copy, and hands it to the components inside. The examples use version 5.105.0. Here it loads the shop's teas three at a time:

An infinite list with TanStack QueryNext.js
app/api/teas/
app/tea-list.tsx
"use client"; import { useInfiniteQuery } from "@tanstack/react-query";import { fetcher } from "./fetcher"; type TeaPage = { teas: string[]; nextPage: number | null }; export default function TeaList() {  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({    queryKey: ["teas"],    queryFn: ({ pageParam }): Promise<TeaPage> => fetcher(`/api/teas?page=${pageParam}`),    initialPageParam: 1,    getNextPageParam: (lastPage) => lastPage.nextPage,  });  if (!data) return <p>Loading teas…</p>;  return (    <>      <ul>        {data.pages.flatMap((page) => page.teas).map((tea) => (          <li key={tea}>{tea}</li>        ))}      </ul>      {hasNextPage && (        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>          Load more        </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
All teas
  • Green tea
  • Mint tea
  • Lemon tea
  • Oolong tea
  • Black tea
  • Rooibos
Focus
nothing: the page itself
Scrolled
0 px down

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

  1. GET/api/teas?page=2200application/json

Start with app/providers.tsx. A provider keeps its client in React context, and context only works in Client Components, so line 1 is "use client". Lines 8 to 13 choose the client. On the server, line 10 makes a new one for each render, so two visitors never share a copy. In the browser, line 11 makes one the first time and keeps it for the whole visit. In app/layout.tsx, line 9 puts Providers around every page. The layout and the pages stay Server Components: they pass through as children, as Composing Server and Client Components taught.

Now app/tea-list.tsx. Lines 9 to 14 call useInfiniteQuery, TanStack Query's hook for a list that arrives in pages. Line 10 is the key, ["teas"]; TanStack Query's keys are arrays. Line 11 loads one page: it asks /api/teas with the page number. Line 12 starts at page 1. Line 13 reads the next page's number from the last answer: app/teas.ts sends nextPage: null with the last page, which means there's no more. Line 19 joins every page into one list, and lines 23 to 27 show the Load more button while there's a next page.

The Steps tab is open on step 2, the click on Load more. It sent one request, GET /api/teas?page=2, and the list grew to six teas without asking for page 1 again. The button is gone: the answer said nextPage: null. Step 1 shows the first page's request, GET /api/teas?page=1, sent after the page loaded. In the Network tab, the second response is /api/teas?page=2 asked for directly, without a page around it: three teas and "nextPage":null.

Both libraries do the same jobs with different names. Each row is one job, from each library's docs and Next.js's guides:

The same jobs in SWR and TanStack Query
The jobSWRTanStack Query
Read one keyuseSWR(key, fetcher)useQuery({ queryKey, queryFn })
Ask again on a timerrefreshInterval: 2000refetchInterval: 2000
Load a list page by pageuseSWRInfinite from swr/infiniteuseInfiniteQuery
Does it need a provider?No. SWRConfig is there when you want shared settings.Yes. A QueryClientProvider in a Client Component.
Start with data from the server<SWRConfig value={{ fallback }}>prefetchQuery, then <HydrationBoundary state={dehydrate(queryClient)}>

Choose either. SWR is smaller and needs no provider. TanStack Query has more options for lists, changes and the cache. What matters for Next.js is the same for both: they run in Client Components, after hydration, and they ask addresses the browser can reach.

Route Handlers answer the browser

Code in the browser can't call getOrder(). That function runs on the server, next to the kitchen (in a real shop, the database and its password). The browser can only send HTTP requests to addresses. A Route Handler gives it one. Four things to know first; Route Handlers, later in this stage, covers the rest.

  • The file's place is the address. app/api/order/route.ts answers /api/order. The api folder is a habit that keeps data addresses apart from pages; Next.js doesn't require it.
  • Each exported function is an HTTP method. Line 3 of route.ts exports GET, so the file answers GET requests.
  • It answers with a web Response. Line 4 uses Response.json(), which turns the order into JSON and sets content-type: application/json, as the recordings show.
  • It's public. Like a page, a Route Handler answers anyone who asks for its address, not only your pages. Check who's asking inside it before you send private data. The Building real apps stage covers sign-in and security.

One detail decides whether it works at all. Look at line 9 of app/orders.ts: await connection(), from next/server, waits for a real request. Try the route with and without it. Each version was built and asked for /api/order four times in a row. Before you click, guess what the fourth check answers without line 9.

Playground: does the Route Handler answer for each request?
The order page, with this version of app/orders.tsNext.js
app/api/order/
app/orders.ts
import { connection } from "next/server"; // A stand-in for the shop's kitchen. It keeps its count on globalThis,// so every check on this server counts, from a page or a Route Handler.const kitchen = globalThis as typeof globalThis & { checks?: number }; export async function getOrder() {  // Wait for a real request, so each call checks now, not during the build.  await connection();  kitchen.checks = (kitchen.checks ?? 0) + 1;  // This pretend order is ready on the fourth check.  return { id: 12, tea: "Mint tea", status: kitchen.checks < 4 ? "Brewing" : "Ready for pickup" };}
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 (4/4)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ƒ /api/order ○  (Static)   prerendered as static contentƒ  (Dynamic)  server-rendered on demand
Try it yourself
getOrder waits for a request

Line 9 waits for a real request, so the build can't run the handler ahead of time: ƒ /api/order. Each check runs getOrder() again, and the fourth answers Ready for pickup.

Both versions were built with next build from Next.js 16.4.3 and asked for /api/order four times in a row. Only app/orders.ts changes. The Network tab shows the fourth answer; pick the others above the page. Reset goes back to the first version.

Without connection(), nothing in the handler depends on the request, so next build ran it once, like a page, and kept its answer: ○ /api/order, prerendered as static content. Every check gets the build's Brewing, with x-nextjs-cache: HIT, and the order never gets ready. With it, the build prints ƒ /api/order, server-rendered on demand: getOrder() runs for each request, and the fourth answers Ready for pickup. Each row compares the two:

getOrder with and without connection()
QuestionWith connection()Without it
What does the build print?ƒ /api/order: server-rendered on demand.○ /api/order: prerendered as static content.
When does getOrder() run?For every request to /api/order.Once, during next build.
What do four checks answer?Brewing three times, then Ready for pickup.Brewing four times, with x-nextjs-cache: HIT.

Reading the request does the same. In the tea list, line 5 of app/api/teas/route.ts reads the page number from request.nextUrl.searchParams, the address's query, and its build prints ƒ /api/teas. A Route Handler for the browser is for the browser only: a Server Component calls getOrder() directly instead of fetching its own Route Handler, as Fetching data in Server Components explains.

Start on the server, keep going in the browser

The order page has one weak moment: Checking your order… until the JavaScript has loaded and the first request has answered. The server could check the order while it renders the page, and the browser could take over from there. SWR supports that with a fallback: a first value for a key, given by the server. Here is the page:

The order page, checked on the server firstNext.js
app/api/order/
app/page.tsx
import { Suspense } from "react";import { SWRConfig } from "swr";import { getOrder } from "./orders";import OrderStatus from "./order-status"; export default function OrderPage() {  return (    <main>      <h1>Order 12</h1>      <Suspense fallback={<p>Checking your order…</p>}>        <SWRConfig value={{ fallback: { "/api/order": getOrder() } }}>          <OrderStatus />        </SWRConfig>      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-stale-time 300
x-nextjs-prerender 1
x-nextjs-postponed 1
X-Powered-By Next.js
Cache-Control private, no-cache, no-store, max-age=0, must-revalidate
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 14 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/1j_b1xq_woyiy.js" async=""></script>
    <script src="/_next/static/chunks/2ki52vdnbo2ft.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/3tlz-4f0fx3-p.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <h1>Order 12</h1>
      <!--$?-->
      <template id="B:0"></template>
      <p>Checking your order…</p>
      <!--/$-->
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script>…</script>
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/3cfn99dbrcvvp.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
    <div hidden="" id="S:0">
      <p>Mint tea<!-- -->: <!-- -->Brewing</p>
    </div>
    <script>…</script>
    <script>…</script>
  </body>
</html>

Line 11 of app/page.tsx calls getOrder() without await and puts the promise under the key "/api/order" in SWRConfig's fallback. SWRConfig is a Client Component that SWR exports, so the promise travels to the browser as a prop, the way a promise did in Composing Server and Client Components. The key matches the one OrderStatus reads, which matters: with a different key, SWR would ignore the fallback. In app/order-status.tsx, line 7 adds suspense: true, so the component waits for the promise inside the Suspense on lines 10 to 14 of the page, and data is always there.

The Terminal tab prints ◐ /: prerendered as static HTML with dynamic server-streamed content. The Network tab shows the HTML: Checking your order… in the shell, and further down, in a hidden <div> that React swaps in, Mint tea: Brewing, the way Streaming with Suspense showed. The status arrived with the page, before any JavaScript ran. Now open the Steps tab. Step 1 opened the page, and its requests are the page and one GET /api/order: SWR treats the server's copy as stale and asks again after hydration. Step 2 is the next check, two seconds later: Ready for pickup. The stand-in counts every check, from the page and from the Route Handler, so this was the fourth: the recorder's own request for /, Chrome's page load, then two from SWR.

Left, a server window where getOrder() runs while the page renders. An arrow labeled HTML and payload goes to a browser window that already shows Mint tea: Brewing. From the browser, an arrow goes to a tag for /api/order, labeled: SWR asks after hydration, then every 2 seconds.servergetOrder()HTML andpayloadbrowserMint tea: Brewing/api/orderSWR asks afterhydration, thenevery 2 seconds
The server checks the order while it renders the page, so the status arrives with the page. Then SWR takes over in the browser: it asks /api/order once after hydration, and again every 2 seconds.

Step through both order pages. Before you start, guess after which step each visitor first sees the status.

Playground: when does the status first show?
Who does whatBrowser only
What the visitor seesafter step 1
localhost:3000

A blank page

React isn't running yet: clicks do nothing.

The page isn't complete yet.

Step 1 of 6
Browser: The visitor opens the order page

The browser asks the server for /.

Each step is one thing that happens, in order. The screens are from the recordings of the two order pages above: the HTML as it arrived, and Chrome's page after each browser step. Times aren't to scale. Reset goes back to the browser-only page's first step.

Browser only, the status shows after step 6: the HTML, then the JavaScript, then a request and its answer. Started on the server, it shows after step 3, inside the first response. After that, both pages keep checking the same way.

TanStack Query has the same pattern with other names. The server makes a QueryClient, starts prefetchQuery on it, and turns the client's copies into plain data with dehydrate(queryClient), which it passes to <HydrationBoundary state={…}>; the Client Components inside start from those copies. To start the prefetch without await and stream the data in later, dehydrate also needs a shouldDehydrateQuery option that keeps pending queries, which it leaves out by default (TanStack Query 5.40 and later can do this). Next.js's TanStack Query guide and TanStack Query's own server rendering guide show both. Choose between the patterns by what happens after the page loads:

  • Read once, never refreshed: pass a promise and read it with use(). No library.
  • Needed in the first HTML, then kept fresh in the browser: start on the server and hand the library the value.
  • Not needed until the visitor acts: fetch in the browser only, like the search box in the exercises.

Planning the tea shop's data

A real tea page mixes all of these. The description and the price come from the server, in the HTML. The reviews come from the server too, as a promise. The stock count keeps changing, so the browser asks for it. Search results wait for the visitor's letters. Sort these pieces of the shop: which ones does the server load, and which ones does the browser fetch?

Server or browser?
  • Each tea's description, which search engines should read.
  • How many tins are left, updated while the visitor looks at the page.
  • Teas that match the letters the visitor types.
  • The price list on the home page.
  • The next six teas, after a click on Load more.
  • Reviews a Client Component shows once, with a Show all button.
Try it yourself
0 of 6 correct

Sort each piece of data: load it on the server, or fetch it in the browser after the page loads.

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

Every card that went to the browser has the same reason: something happens after the page has loaded. Time passes, the visitor types, the visitor clicks. When nothing like that happens, the server is the better place.

Common mix-ups

  • "In Next.js, fetching in the browser is wrong." No. The server is the default, but live data, search results and load-more lists belong in the browser. Next.js's docs have a guide for it.
  • "A Client Component can call my server function to get data." No. Its code runs in the browser, which reaches the server only through an address, like a Route Handler's.
  • "A GET Route Handler runs for every request." Not always. One that reads nothing from the request, and does no uncached or request-time work, is prerendered during the build, ○, and sends that answer every time.
  • "Two components that fetch the same data send two requests." Not at the same moment, with SWR or TanStack Query. Components that read one key share its cached copy, and a request already on its way for that key is shared instead of sent twice, as the badge showed. Later refreshes still send new requests.
  • "Data the browser fetched is in the HTML." No. The order page's HTML said Checking your order…. Client Components are prerendered too, but data the browser fetches after hydration isn't in the first HTML.
  • "SWR and TanStack Query replace Server Components." No. They run in Client Components after hydration. The first data still comes from the server when you hand it over.

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-upCount the requests for one key

The layout shows this badge, and the page shows OrderStatus, which reads "/api/order" too. How many GET /api/order requests does Chrome send when the page loads?

Starter codeTSX
"use client";

import useSWR from "swr";
import { fetcher } from "./fetcher";

export default function OrderBadge() {
  const { data } = useSWR<{ status: string }>("/api/order", fetcher);
  return <p>Your order: {data ? data.status : "…"}</p>;
}

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

    Exercise 2 · Warm-upPlace a tea's description

    Each tea page has a long description that search engines should read. It changes a few times a year. Should the server load it, or should the browser fetch it?

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

      Exercise 3 · PracticeFind why the order never gets ready

      With this app/orders.ts, the order page says Brewing forever, however long the visitor waits. Which line is missing?

      Starter codeTypeScript
      // A stand-in for the shop's kitchen. It keeps its count on globalThis,
      // so every check on this server counts, from a page or a Route Handler.
      const kitchen = globalThis as typeof globalThis & { checks?: number };
      
      export async function getOrder() {
        kitchen.checks = (kitchen.checks ?? 0) + 1;
        // This pretend order is ready on the fourth check.
        return { id: 12, tea: "Mint tea", status: kitchen.checks < 4 ? "Brewing" : "Ready for pickup" };
      }
      

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

        Exercise 4 · PracticeWrite a Route Handler for a search box

        The shop wants a search box. Write app/api/search/route.ts so that GET /api/search?q=mi answers with the matching teas. Which symbol does the build print for /api/search?

        Starter codeTypeScript
        // app/api/search/route.ts
        // Answer GET /api/search?q=mi with the teas whose names contain "mi".

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

          Exercise 5 · ChallengeSearch only when there's something to search

          This search box asks the server as soon as the page loads, with an empty q, and shows every tea. Change line 9 so it asks nothing until the visitor types. What do you pass as the key while the box is empty?

          Starter codeTSX
          "use client";
          
          import { useState } from "react";
          import useSWR from "swr";
          import { fetcher } from "./fetcher";
          
          export default function TeaSearch() {
            const [query, setQuery] = useState("");
            const { data } = useSWR<string[]>(`/api/search?q=${encodeURIComponent(query)}`, fetcher);
            return (
              <>
                <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search teas" />
                <ul>
                  {(data ?? []).map((tea) => (
                    <li key={tea}>{tea}</li>
                  ))}
                </ul>
              </>
            );
          }
          

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

            Exercise 6 · ChallengePut the first status in the HTML

            This is the browser-only order page, ○ /. Change it so the first status is in the HTML and SWR keeps checking afterwards. Which symbol does the build print for / then?

            Starter codeTSX
            import OrderStatus from "./order-status";
            
            export default function OrderPage() {
              return (
                <main>
                  <h1>Order 12</h1>
                  <OrderStatus />
                </main>
              );
            }
            

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

              Check your understanding

              For each question, ask when the data changes, and which side can reach it.

              The Fetching in the browser quiz · 7 questionsScore: first tries count
              1. Question 1 of 7A tea's description is the same for everyone and should be in the first HTML. Where should it load?

                Choose an answer to see the explanation.

              2. Question 2 of 7This badge and OrderStatus both read the key /api/order on the same page. How many requests does the page send when it loads?

                Read the code, then predictTSX
                "use client";
                
                import useSWR from "swr";
                import { fetcher } from "./fetcher";
                
                export default function OrderBadge() {
                  const { data } = useSWR<{ status: string }>("/api/order", fetcher);
                  return <p>Your order: {data ? data.status : "…"}</p>;
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7This app/orders.ts makes GET /api/order print ○ in the build. What does the browser get when it asks four times?

                Read the code, then predictTypeScript
                // A stand-in for the shop's kitchen. It keeps its count on globalThis,
                // so every check on this server counts, from a page or a Route Handler.
                const kitchen = globalThis as typeof globalThis & { checks?: number };
                
                export async function getOrder() {
                  kitchen.checks = (kitchen.checks ?? 0) + 1;
                  // This pretend order is ready on the fourth check.
                  return { id: 12, tea: "Mint tea", status: kitchen.checks < 4 ? "Brewing" : "Ready for pickup" };
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7Why can't OrderStatus, a Client Component, call getOrder() from app/orders.ts itself to check the order?

                Choose an answer to see the explanation.

              5. Question 5 of 7In the infinite list, what does a click on Load more send?

                Choose an answer to see the explanation.

              6. Question 6 of 7This OrderStatus runs under <SWRConfig value={{ fallback: { "/api/order": getOrder() } }}>. What happens right after the page loads?

                Read the code, then predictTSX
                "use client";
                
                import useSWR from "swr";
                import { fetcher } from "./fetcher";
                
                export default function OrderStatus() {
                  const { data } = useSWR("/api/order", fetcher, { suspense: true, refreshInterval: 2000 });
                  return (
                    <p>
                      {data.tea}: {data.status}
                    </p>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7A Client Component only needs reviews once, and never refreshes them. What's the simplest way?

                Choose an answer to see the explanation.

              Key takeaways

              • Load data on the server by default. Fetch in the browser when the data changes while the page is open, depends on what the visitor does, or is only needed later.
              • Browser fetching happens in Client Components, after hydration. Data fetched there isn't in the HTML, and the browser's request list shows each fetch.
              • SWR and TanStack Query keep one copy per key: components that read it share the copy and the request already on its way, and the copy refreshes on a timer you set. TanStack Query needs a provider in a Client Component.
              • The browser asks Route Handlers, route.ts files that answer with data. A GET handler that reads nothing from the request and does no uncached or request-time work is prerendered; connection() or reading the request makes it run each time.
              • To have data in the first HTML and fresh afterwards, start it on the server and hand it to the library: SWR's fallback, or TanStack Query's dehydrate(queryClient) in a HydrationBoundary.

              Remember the one-liner.
              The server sends the page once; the browser asks again only for what changes after that.

              Coming next: Server Actions. Fetching reads data; Server Actions change it, with a function the browser calls like any other, and a form that works before the JavaScript has loaded.

              CompleteFrontend Clear concepts. Working examples.