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.
- 01Choose where data loadsTell data that belongs in the browser (live updates, load-more lists, after-load data) from data the server should load.
- 02Fetch 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.
- 03Give 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.
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> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
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
GET/200a whole pageGET/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.
/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.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
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:
| The data | Where and when it loads | In short |
|---|---|---|
| The tea's name, price and description | On 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 button | On 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 open | In the browser, asked again every few seconds. | Browser |
| More teas, after the visitor presses Load more | In the browser, one page at a time, when the visitor asks. | Browser |
| Teas that match what the visitor is typing | In the browser, after each change to the search box. | Browser |
| The signed-in visitor's past orders, in a panel they open later | In the browser, when the panel opens. Nobody needs it in the first HTML. | Browser |
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", thefetcher, and options. SWR keeps one copy of the data for each key. When it has no copy yet, or the copy is old, it callsfetcher("/api/order").<Order>tells TypeScript the shape of the data. - Lines 2 to 5 of
app/fetcher.tsare 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:
errorif the fetcher threw, and nodatawhile 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:
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>;}Each step was taken in real Chrome against next start. Pick one to see the page after it.
Your order: Brewing
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
GET/200a whole pageGET/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.
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:
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> )} </> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- 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
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 job | SWR | TanStack Query |
|---|---|---|
| Read one key | useSWR(key, fetcher) | useQuery({ queryKey, queryFn }) |
| Ask again on a timer | refreshInterval: 2000 | refetchInterval: 2000 |
| Load a list page by page | useSWRInfinite from swr/infinite | useInfiniteQuery |
| 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.tsanswers/api/order. Theapifolder 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.tsexportsGET, so the file answersGETrequests. - It answers with a web
Response. Line 4 usesResponse.json(), which turns the order into JSON and setscontent-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.
app/orders.tsNext.jsapp/orders.tsimport { 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" };}$ 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 demandLine 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.
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:
| Question | With 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:
app/page.tsximport { 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> );}GET /200
- Response headers
Varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encodingx-nextjs-stale-time300x-nextjs-prerender1x-nextjs-postponed1X-Powered-ByNext.jsCache-Controlprivate, no-cache, no-store, max-age=0, must-revalidateContent-Typetext/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.
/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.
Browser onlyafter step 1A blank page
React isn't running yet: clicks do nothing.
The page isn't complete yet.
The browser asks the server for /.
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?
- 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.
Sort each piece of data: load it on the server, or fetch it in the browser after the page loads.
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.
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?
"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>;
}
One. The badge and OrderStatus read the same key, "/api/order", so SWR sends one request and gives both components the answer. The recording's first step lists one GET /api/order.
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?
On the server, in a Server Component. The description is the same for everyone, doesn't change while the page is open, and search engines should read it, so it belongs in the first HTML. A browser fetch would leave it out.
With this app/orders.ts, the order page says Brewing forever, however long the visitor waits. Which line is missing?
// 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" };
}
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" };
}
await connection(); at the start of getOrder(), imported from next/server. Without it, the build runs the handler once (○ /api/order) and every check gets the same Brewing. With it, the build prints ƒ /api/order and the fourth check answers Ready for pickup, as both recordings show.
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?
// app/api/search/route.ts
// Answer GET /api/search?q=mi with the teas whose names contain "mi".import type { NextRequest } from "next/server";
const teas = ["Green tea", "Mint tea", "Lemon tea", "Oolong tea"];
export async function GET(request: NextRequest) {
const query = (request.nextUrl.searchParams.get("q") ?? "").toLowerCase();
return Response.json(teas.filter((tea) => tea.toLowerCase().includes(query)));
}
Line 6 reads q from the address's query, and line 7 answers with the teas that contain it, as JSON. Reading the request makes the handler run for each request, so the recorded build prints ƒ /api/search. In Chrome, typing mi sent GET /api/search?q=mi, and the page showed Mint tea.
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?
"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>
</>
);
}
"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[]>(query ? `/api/search?q=${encodeURIComponent(query)}` : null, fetcher);
return (
<>
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search teas" />
<ul>
{(data ?? []).map((tea) => (
<li key={tea}>{tea}</li>
))}
</ul>
</>
);
}
null: query ? /api/search?q=… : null. With the starter, the recording shows GET /api/search?q= as soon as the page loads, and all four teas before anyone typed. With a null key, the first step sends nothing, and typing mi sends one GET /api/search?q=mi.
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?
import OrderStatus from "./order-status";
export default function OrderPage() {
return (
<main>
<h1>Order 12</h1>
<OrderStatus />
</main>
);
}
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>
);
}
◐. The page now starts getOrder() for each request, inside Suspense, so the heading and fallback are prerendered and the status streams in: the recorded build prints ◐ /, and the HTML holds Mint tea: Brewing. OrderStatus adds suspense: true to read the fallback.
Check your understanding
For each question, ask when the data changes, and which side can reach it.
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.
Question 2 of 7This badge and
OrderStatusboth read the key/api/orderon 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.
Question 3 of 7This
app/orders.tsmakesGET /api/orderprint○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.
Question 4 of 7Why can't
OrderStatus, a Client Component, callgetOrder()fromapp/orders.tsitself to check the order?Choose an answer to see the explanation.
Question 5 of 7In the infinite list, what does a click on Load more send?
Choose an answer to see the explanation.
Question 6 of 7This
OrderStatusruns 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.
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.tsfiles that answer with data. AGEThandler 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'sdehydrate(queryClient)in aHydrationBoundary.
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.