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

The 'use cache' directive

Put "use cache" on files, components and functions, and see what becomes the cache key, what can be cached, and how children pass through a cache.

By the end, you can
  • 01
    Place the directiveCache a function, a component or a whole file, and know the rules for each: async, and every export in a cached file.
  • 02
    Predict the cache keySay which calls share one stored copy: the same build, the same function, and the same arguments and captured values.
  • 03
    Keep the inputs storablePass plain values, read request data outside, let children pass through, and choose between caching data and caching UI.

One tea, fetched once

Cache Components showed the model: work that waits is fresh for each request, unless you cache it. "use cache" is how you cache it. It's a directive, a string at the top of a function or a file, and it tells Next.js: keep what this returns, and give the same inputs the same stored copy. Here, the tea shop's Teas page shows one tea, picked by the address, like /teas?tea=mint. It's a real Next.js 16.4.3 project, built with next build and run with next start, and the result is a recording.

One tea at a time, from a cached getTeaNext.js
app/teas/lib/
lib/supplier.ts
const api = process.env.SUPPLIER_API_URL; export type Tea = { id: string; name: string; price: number }; export async function getTea(id: string): Promise<Tea> {  "use cache";  const response = await fetch(`${api}/teas/${id}`);  return response.json();}
Recorded from Next.js 16.4.3
localhost:3000/teas?tea=mintTea shop
Home Teas
Our teas

Mint tea: $3

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

In lib/supplier.ts, the file shown first, getTea on lines 5 to 9 asks the tea supplier's API for one tea. Line 6, "use cache", is the whole change: it caches the function. Open app/teas/page.tsx. TeaDetails, on lines 4 to 12, reads tea from the address on line 5, and line 6 calls getTea with it. The address is only known when a request comes in, so lines 18 to 20 put TeaDetails in Suspense.

The supplier is a stand-in that ran on the same computer and kept every request it got. Three visits came in: ?tea=mint, ?tea=mint again, and ?tea=green. Pick each one above the page in the Browser tab. The supplier was asked for /teas/mint once and /teas/green once. The second mint visit got the copy the first one stored. Nothing was asked during the build, because the build didn't know which tea anyone would pick.

A call to getTea with mint goes to a key made of three parts: the build's ID, getTea's ID, and the argument mint. The key points into the stored copies, which hold two entries: mint, with Mint tea, and green, with Green tea.getTea("mint")the keybuild IDgetTea's ID"mint"storedmint: Mint teagreen: Green tea
getTea("mint") is looked up by its key: the build, the function, and the argument. The three visits made two keys, so two stored copies, and the supplier was asked twice.
Real-life analogyA binder of the supplier's answers

A tea shop clerk phones the supplier with an order form: the price form, with "mint" written on it. The answer gets filed in a binder, under that form and that word. When a second customer asks about mint, the clerk finds the answer in the binder and doesn't phone. A question about green tea is a different word on the form, so it gets its own call and its own page. Each season, the shop starts a new binder.

In real life: The order form the clerk fills in, like the price form
In Next.js: The cached function, getTea
In real life: What's written on the form: mint
In Next.js: The arguments: "mint"
In real life: Filing each answer under the form and what was written on it
In Next.js: Storing the result under its cache key
In real life: A new binder every season
In Next.js: A new build: no copy survives it
The main idea

"use cache" stores what an async function or component returns, under a key made of the build, the function, and its inputs. A later call with the same key gets the stored copy instead of running again. So everything that goes into the key, and everything that comes out, must be values Next.js can store.

Files, components and functions

The directive goes at the very top of a function's body, or at the very top of a file. That gives three places:

  • A function, like getTea: its result, usually data, is stored.
  • A component, which is a function too: its props are its arguments, and what it renders is its result.
  • A file: every function the file exports is cached, as if each one started with the directive.

In all three, the function must be async, or the build stops. Each button below is the same shop with the directive in a different place, visited by the same three visitors. Before you click, guess how many times each version asks for mint.

Playground: where does the directive go?
The Teas page, with the directive in this placeNext.js
app/teas/lib/
lib/supplier.ts
const api = process.env.SUPPLIER_API_URL; export type Tea = { id: string; name: string; price: number }; export async function getTea(id: string): Promise<Tea> {  const response = await fetch(`${api}/teas/${id}`);  return response.json();}
Recorded from Next.js 16.4.3
localhost:3000/teas?tea=mintTea shop
Home Teas
Our teas

Mint tea: $3

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

Try it yourself
Nowhere

Nothing is cached. Over the three visits, the supplier was asked for /teas/mint twice and /teas/green once: one request for each visit.

Every version was built with next build from Next.js 16.4.3 and visited with ?tea=mint, ?tea=mint and ?tea=green, against the same stand-in supplier. Reset goes back to the first version.

The three places that build all store a copy for each tea: the supplier was asked once for mint and once for green. Without the directive, it was asked for mint twice. A file-level directive turns every exported function into a cached one, so a small helper like teaUrl, which isn't async, stops the build. Move such a helper to a file of its own, or don't export it.

A page.tsx or layout.tsx file can start with the directive too. Next.js's docs say each one is then cached on its own, and a cached layout's children pass through: they're not cached with the layout.

Arguments and closures become the key

A cache key is the label a stored copy is filed under. When a cached function is called, Next.js builds the key from the call and looks for a copy with that key. If there is one, the function's body doesn't run. Each row is one part of the key in Next.js 16.4.3:

What goes into a cache key
PartWhat it isWhat gives a new key
The build's IDA name Next.js gives each build (or your deploymentId, if you set one).Every new build, so a new deploy starts with no stored copies.
The function's IDA name the compiler gives each cached function, from where it is in your code and how it's declared.Moving the function, or changing its name or parameters.
The argumentsWhat the call passes in: props for a component, arguments for a function.Any different value: "mint" and "green" are two entries.
Captured valuesValues from outside the function that its code uses, like tea in getDetails.Any different value, the same as an argument.

In Next.js's own code, the key is one line, in use-cache-wrapper.ts:

The cache key's parts, from use-cache-wrapper.ts at v16.4.3TypeScript
const cacheKeyParts: CacheKeyParts = [id, args, implementationPart]

const encodeCacheKeyParts = () =>
  encodeReply(cacheKeyParts, {
    temporaryReferences,
    signal: hangingInputAbortSignal,
  })

id is the function's ID, and args its arguments, captured values included. implementationPart is, by default, the build's ID in a production build (lines 3917 to 3931), with an extra part in next dev that changes when you edit a Server Component. Next.js turns the three into one string with React's encodeReply, the same encoding a Server Action's arguments use (lines 2291 to 2297). That's why the same values are allowed in both. temporaryReferences is where values that can only pass through, like JSX, are set aside instead of going into the key.

Values from outside the function

A function can use variables from the code around it: that's a closure. A cached closure's key includes the values it uses, as if they were arguments. Here getDetails is defined inside TeaDetails and takes no arguments at all:

A cached function that uses a value from outside itNext.js
app/teas/lib/
app/teas/page.tsx
import { Suspense } from "react"; const api = process.env.SUPPLIER_API_URL; async function TeaDetails({ searchParams }: Pick<PageProps<"/teas">, "searchParams">) {  const { tea = "green" } = await searchParams;   async function getDetails() {    "use cache";    const response = await fetch(`${api}/teas/${tea}`);    return response.json();  }   const details = await getDetails();  return (    <p>      {details.name}: ${details.price}    </p>  );} export default function TeasPage({ searchParams }: PageProps<"/teas">) {  return (    <main>      <h1>Our teas</h1>      <Suspense fallback={<p>Finding the tea…</p>}>        <TeaDetails searchParams={searchParams} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/teas?tea=mintTea shop
Home Teas
Our teas

Mint tea: $3

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

Line 6 reads tea from the address. getDetails, on lines 8 to 12, is cached on line 9, and line 10 uses tea in the supplier's address. The same three visits asked for mint once and green once, and the green visit showed Green tea: $4. If tea weren't in the key, the green visit would have received the mint copy. Next.js passes captured values in as hidden arguments, so they follow the same rules as arguments.

Where the copies are kept

During next build, a cached result that a page needs goes into that page's prerendered output. While next start runs, copies made for requests are kept in the server's memory: by default, a store that drops the least recently used copies when it passes 50 MB, the cacheMaxMemorySize option. The build's own memory is gone by then: it isn't carried into next start as it was. This version shows a tea of the day on the home page, with getTea("mint"):

The home page's tea of the day, cached during the buildNext.js
app/teas/lib/
app/page.tsx
import { getTea } from "@/lib/supplier"; export default async function Home() {  const tea = await getTea("mint");  return (    <main>      <h1>Tea shop</h1>      <p>Tea of the day: {tea.name}</p>    </main>  );}
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)      Revalidate  Expire┌ ○ /                   15m      1y├ ○ /_not-found└ ◐ /teas ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

The build asked for mint once, for the home page, and printed ○ / 15m 1y. The home page itself is served from what the build prerendered, with the tea already in it. Then a visit to another route, /teas?tea=mint, asked for mint again: nothing that route kept from the build held that copy.

A route can keep cached results of its own, though. When the build prerenders a ◐ route, it saves the route's postponed state, what Next.js needs to finish the page for a request, and with it the cached results the build filled for that route (Next.js calls them the resume data cache; postponed-state.ts). When that same route is finished for a visit, a cached call looks there first (use-cache-wrapper.ts, line 2470), so it can reuse what its own build filled. A new build has a new ID, so no copy survives a deploy, and an app on several servers has a memory store on each. Private and remote caches shows a store all servers share.

15m comes from the default lifetime, which applies when a cached function doesn't call cacheLife. The docs give its three times:

  • stale, 5 minutes: how long the browser may reuse a copy without asking the server.
  • revalidate, 15 minutes: after this, the server should make a new copy.
  • No expire: no time after which an old copy must stop being used.

What happens at the revalidate time depends on where the copy is kept. In Next.js 16.4.3, the default in-memory store in next start drops a copy once it's past its revalidate time (default.ts, lines 104 to 125), so the next call after 15 minutes runs the function again. That's this version's store, not a rule of cacheLife. 1y is Next.js's expireTime option, a year, which the build uses when nothing set a limit. The docs recommend calling cacheLife in every cached function, so its lifetime is written where you can see it; Cache lifetimes and tags teaches it.

What can go in and come out

Everything in the key has to be turned into text, and the result has to be stored and sent again later. So a cached function's inputs and output must be serializable: values that can be written down and read back as the same values. The docs give the rules: arguments follow React's rules for a Server Action's arguments, and the result follows React's rules for props passed to a Client Component (Composing Server and Client Components). Each row is one kind of value:

What a cached function can take and give back
ValueAs an argumentAs the result
Text, numbers, true, false, null, undefinedYes, and they become part of the key.Yes.
Plain objects and arrays of thoseYes.Yes: getTea returns the object it got from JSON.
Date, Map, SetYes, the docs say.Yes, the docs say.
JSX, like <p>Hi</p>Only passed through: not part of the key, and not read.Yes: a cached component returns JSX.
FunctionsOnly passed through: calling one fails.Only one passed through, like a Server Action handed on to a Client Component.
Instances of a classOnly passed through: reading one fails.No: the result can't be stored.
Playground: what can a cached function take and return?
The Teas page, with this version of lib/supplier.tsNext.js
app/teas/lib/
lib/supplier.ts
const api = process.env.SUPPLIER_API_URL; export type Tea = { id: string; name: string; price: number }; export async function getTea(id: string): Promise<Tea> {  "use cache";  const response = await fetch(`${api}/teas/${id}`);  return response.json();}
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└ ◐ /teas ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
Try it yourself
A string in

getTea gets a string and returns plain data from JSON. Both can be stored: the visit shows Mint tea: $3.

Every version was built with next build from Next.js 16.4.3 and visited with ?tea=mint. After the build, the Terminal shows what next start printed during the visit. Reset goes back to the first version.

All three mistakes built: ◐ /teas. The cached call sits inside a part that waits for the request, so the build never ran it, and the error came on the first visit. The page's HTML kept Finding the tea… and the tea never arrived; the server printed the error, which the Terminal tab shows after the build. Each message, as next start printed it, names the problem:

  • A class instance in: Cannot access value on the server. You cannot dot into a temporary client reference from a server component. You can only pass the value through to the client. The TeaId arrived as a reference that can only be passed through, so reading id.value failed before the supplier was asked.
  • A function in: Attempted to call a temporary Client Reference from the server but it is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component. The supplier answered, then calling format failed.
  • A class instance out: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported. The supplier answered, then storing the Tea failed.

The fix is the same each time: pass and return plain values. getTea(id: string) takes the tea's id, not an object around it, and returns the JSON as it came.

Request data stays outside

A stored copy is shared by every request with the same key, so it can't depend on one request. Reading request data inside a cached function is an error:

A cached function that reads a cookie (an error on purpose)Next.js
app/teas/lib/
lib/supplier.ts
import { cookies } from "next/headers"; const api = process.env.SUPPLIER_API_URL; export async function getFavorite() {  "use cache";  const id = (await cookies()).get("favorite")?.value ?? "green";  const response = await fetch(`${api}/teas/${id}`);  return response.json();}
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: Route "/teas": `cookies()` can't be read inside `"use cache"`. Read it outside the cached function and pass what you need as an argument.Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache    at g (lib/supplier.ts:7:21)   5 | export async function getFavorite() {   6 |   "use cache";>  7 |   const id = (await cookies()).get("favorite")?.value ?? "green";     |                     ^   8 |   const response = await fetch(`${api}/teas/${id}`);   9 |   return response.json();  10 | } {  digest: '3275836682'}Error: Route "/teas": `cookies()` can't be read inside `"use cache"`. Read it outside the cached function and pass what you need as an argument.Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache    at g (lib/supplier.ts:7:21)   5 | export async function getFavorite() {   6 |   "use cache";>  7 |   const id = (await cookies()).get("favorite")?.value ?? "green";     |                     ^   8 |   const response = await fetch(`${api}/teas/${id}`);   9 |   return response.json();  10 | } {  digest: '3275836682'}To get a more detailed stack trace and pinpoint the issue, try one of the following:  - Start the app in development mode by running `next dev`, then open "/teas" in your browser to investigate the error.  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/page: /teas, exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null

Line 7 reads the favorite cookie inside getFavorite, which is cached on line 6. The build stopped: cookies() can't be read inside "use cache". Read it outside the cached function and pass what you need as an argument. headers() and searchParams have messages of their own that say the same (use-cache-messages.ts, lines 35 to 63). The docs add that the rule follows the calls: a helper the cached function calls can't read them either. Read the cookie in the component, inside Suspense, and pass its value: that's the "both" pattern from Cache Components, and exercise 3 below.

Passing children through a cache

A cached component can take children, or any other prop that holds JSX, and put it in what it renders without looking at it. Then the JSX passes through: it's not part of the key, and not part of the stored copy. Each request fills it in. Here a cached TeaCard shows mint tea with a greeting inside it:

A cached card, with each visitor's greeting passed throughNext.js
app/teas/lib/
app/tea-card.tsx
import type { ReactNode } from "react"; const api = process.env.SUPPLIER_API_URL; export async function TeaCard({ id, children }: { id: string; children: ReactNode }) {  "use cache";  const response = await fetch(`${api}/teas/${id}`);  const tea = await response.json();  return (    <section>      <h2>{tea.name}</h2>      <p>${tea.price} a box</p>      {children}    </section>  );}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas
Mint tea

$3 a box

Picked for you, Asha.

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

In app/tea-card.tsx, TeaCard on lines 5 to 16 is cached on line 6 and fetches the tea on line 7. Line 13 puts children in its output, and nothing else touches them. In app/teas/page.tsx, MintForYou reads the visitor's name from a cookie on line 6 and passes a <p> with it as children, on lines 8 to 10.

Asha and Ben each saw their own name inside the same card, and the supplier was asked for /teas/mint once. The card was stored once; each visit put its own greeting in the hole.

A window for the cached TeaCard holds Mint tea, $3 a box, and a children slot. Two greetings, Picked for you, Asha, and Picked for you, Ben, each have an arrow into the children slot.TeaCardMint tea$3 a box{children}Picked for you, Asha.Picked for you, Ben.filled in for each visit
The card is stored once, with a hole where children go. Asha's visit and Ben's visit each put their own <p> in the hole, so the supplier was asked for mint once.

Only JSX passes through. Text is a value like any argument, so text children become part of the key. The second version below passes the greeting as text:

Playground: JSX or text as children?
The Teas page, with this version of app/teas/page.tsxNext.js
app/teas/lib/
app/teas/page.tsx
import { Suspense } from "react";import { cookies } from "next/headers";import { TeaCard } from "@/app/tea-card"; async function MintForYou() {  const name = (await cookies()).get("name")?.value ?? "friend";  return (    <TeaCard id="mint">      <p>Picked for you, {name}.</p>    </TeaCard>  );} export default function TeasPage() {  return (    <main>      <h1>Our teas</h1>      <Suspense fallback={<p>Picking a tea…</p>}>        <MintForYou />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas
Mint tea

$3 a box

Picked for you, Ben.

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

Try it yourself
A <p> as children

The greeting is a <p> element, passed through the cached card. Asha and Ben each saw their own name, and the supplier was asked for /teas/mint once.

Both versions were built with next build from Next.js 16.4.3 and visited by Asha, then Ben, each with a cookie holding their name. Only app/teas/page.tsx changes. Reset goes back to the first version.

With text, Picked for you, Asha. and Picked for you, Ben. made two keys, and mint was fetched twice. Wrapping the text in an element keeps one copy for every visitor. The docs add one rule: the cached component must not read or change what passes through, only place it. The same works for a Server Action passed through to a Client Component, as long as the cached component doesn't call it.

Caching data or caching UI

Next.js's docs call the two main uses data-level and UI-level caching. On a data function, the directive stores data, and any component can render it. On a component, it stores what the component rendered, for those props. The shop's menu can be sorted by name or by price, from ?sort= in the address. Each version below was visited with name, then price twice:

Playground: cache the component, or cache the data?
The Teas page, with this version of app/teas/page.tsxNext.js
app/teas/lib/
app/teas/page.tsx
import { Suspense } from "react"; const api = process.env.SUPPLIER_API_URL; type Tea = { id: string; name: string; price: number }; async function Menu({ sort }: { sort: string }) {  "use cache";  const response = await fetch(`${api}/menu`);  const teas: Tea[] = await response.json();  const sorted = sort === "price" ? teas.toSorted((a, b) => a.price - b.price) : teas;  return (    <ul>      {sorted.map((tea) => (        <li key={tea.id}>          {tea.name}: ${tea.price}        </li>      ))}    </ul>  );} async function SortedMenu({ searchParams }: Pick<PageProps<"/teas">, "searchParams">) {  const { sort = "name" } = await searchParams;  return <Menu sort={String(sort)} />;} export default function TeasPage({ searchParams }: PageProps<"/teas">) {  return (    <main>      <h1>Our teas</h1>      <Suspense fallback={<p>Loading the menu…</p>}>        <SortedMenu searchParams={searchParams} />      </Suspense>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/teas?sort=priceTea shop
Home Teas
Our teas
  • Mint tea: $3
  • Lemon tea: $3
  • Green tea: $4

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

Try it yourself
Cache the component

Menu is cached, with sort as its prop. Each sort order is its own entry: /menu was asked for twice, once for name and once for price.

Both versions were built with next build from Next.js 16.4.3 and visited with ?sort=name, ?sort=price and ?sort=price. Only app/teas/page.tsx changes. Reset goes back to the first version.

Cached as UI, Menu got one entry for each sort, so the supplier was asked for /menu twice. Cached as data, getMenu has no arguments, so one entry served both orders, and Menu sorted it for each visit. Each row compares the two:

Caching data and caching UI
QuestionData: the directive on a data functionUI: the directive on a component
What's storedThe data the function returns, like the menu's list.What the component rendered: its output for React.
One entry for eachSet of arguments. getMenu() has none, so one entry.Set of props. <Menu sort="price"> and <Menu sort="name"> are two.
Who can use itAny component that calls the function, rendering it its own way.Only that component, with those props.
Good forData many parts of the app read.A part whose rendering is the work, with few prop values.
Two halves. Left, caching the component: two stored entries, Menu sorted by name and Menu sorted by price, and two requests for the menu. Right, caching the data: one stored entry, getMenu, and one request for the menu.Cache the componentstoredMenu sort="name"Menu sort="price"2 requests for /menuCache the datastoredgetMenu()1 request for /menu
Cached as UI, each sort is its own entry, and each entry asked for /menu. Cached as data, one getMenu() entry served both orders.

Start with the data. Cache a component when rendering it is the work you want to save, and its props take few different values. Both can be combined: a cached component can call a cached function.

Will it work in a cache?

Every card below is something the recordings tried inside "use cache". Sort each one by what happened:

Works inside "use cache", or fails?
  • getTea("mint"): a string argument.
  • A cached inner function that uses tea from the function around it.
  • A cached component that returns JSX.
  • Passing <p>Picked for you, Asha.</p> as children.
  • getTea(new TeaId("mint")), then reading id.value inside.
  • Passing a function as an argument, then calling it inside.
  • Returning new Tea(…) from a cached function.
  • await cookies() inside the cached function.
  • "use cache" in a function that isn't async.
Try it yourself
0 of 9 correct

Sort each use of a cached function or component by what Next.js 16.4.3 did with it.

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

The pattern: plain values in and out, request data read outside, JSX passed through untouched, and async always.

Common mix-ups

  • "The cache key is the function's name." No. It's the build, the function, its arguments, and the values it captures. "mint" and "green" are two entries.
  • "Children are cached with the component." No. JSX children pass through and are filled in for each request. Text children are values, and do join the key.
  • "If the build passes, the cached function is fine." Not always. A cached call inside a part that waits for the request first runs during a visit: the class argument failed only then.
  • "Inside Suspense, a cached function can read cookies." No. Request data is never read inside "use cache". Read it outside and pass the value in.
  • "What the build cached, every route can reuse." No. The build's copies went into the prerendered output of the routes that used them. A visit that called getTea("mint") asked the supplier again.
  • "A file-level directive caches the whole module." No. It caches each exported function, and every one must be async.

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 supplier's requests

Three visits open /teas?tea=mint, /teas?tea=mint and /teas?tea=green, and each calls this getTea with the tea. How many requests does the supplier get?

Starter codeTypeScript
const api = process.env.SUPPLIER_API_URL;

export type Tea = { id: string; name: string; price: number };

export async function getTea(id: string): Promise<Tea> {
  "use cache";
  const response = await fetch(`${api}/teas/${id}`);
  return response.json();
}

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

    Exercise 2 · Warm-upFind the missing word

    The build stops at line 3 of this lib/supplier.ts. What's missing?

    Starter codeTypeScript
    const api = process.env.SUPPLIER_API_URL;
    
    export function getTea(id: string) {
      "use cache";
      return fetch(`${api}/teas/${id}`).then((response) => response.json());
    }
    

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

      Exercise 3 · PracticeRead the cookie outside

      The build stops on this cached getFavorite. Fix it so the cookie is read outside the cache. Then Asha and Ben, both with favorite=mint, visit. How many times is the supplier asked for mint?

      Starter codeTypeScript
      import { cookies } from "next/headers";
      
      const api = process.env.SUPPLIER_API_URL;
      
      export async function getFavorite() {
        "use cache";
        const id = (await cookies()).get("favorite")?.value ?? "green";
        const response = await fetch(`${api}/teas/${id}`);
        return response.json();
      }
      

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

        Exercise 4 · PracticePass a plain value

        A visit fails with an error that starts Cannot access value on the server, because getTea takes a TeaId. What should it take instead?

        Starter codeTypeScript
        const api = process.env.SUPPLIER_API_URL;
        
        export class TeaId {
          constructor(public value: string) {}
        }
        
        export async function getTea(id: TeaId) {
          "use cache";
          const response = await fetch(`${api}/teas/${id.value}`);
          return response.json();
        }
        

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

          Exercise 5 · ChallengeKeep the greeting out of the key

          Asha and Ben each make the cached TeaCard fetch mint again, because of line 8. Change the line so they share one copy. How many times is mint fetched for the two of them then?

          Starter codeTSX
          import { Suspense } from "react";
          import { cookies } from "next/headers";
          import { TeaCard } from "@/app/tea-card";
          
          async function MintForYou() {
            const name = (await cookies()).get("name")?.value ?? "friend";
            return (
              <TeaCard id="mint">{`Picked for you, ${name}.`}</TeaCard>
            );
          }
          
          export default function TeasPage() {
            return (
              <main>
                <h1>Our teas</h1>
                <Suspense fallback={<p>Picking a tea…</p>}>
                  <MintForYou />
                </Suspense>
              </main>
            );
          }
          

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

            Exercise 6 · ChallengeOne fetch for every order

            This page caches the menu as UI. Visits sort by name, then price twice. Change it so the supplier is asked for /menu as few times as possible. How many times is that?

            Starter codeTSX
            import { Suspense } from "react";
            
            const api = process.env.SUPPLIER_API_URL;
            
            type Tea = { id: string; name: string; price: number };
            
            async function Menu({ sort }: { sort: string }) {
              "use cache";
              const response = await fetch(`${api}/menu`);
              const teas: Tea[] = await response.json();
              const sorted = sort === "price" ? teas.toSorted((a, b) => a.price - b.price) : teas;
              return (
                <ul>
                  {sorted.map((tea) => (
                    <li key={tea.id}>
                      {tea.name}: ${tea.price}
                    </li>
                  ))}
                </ul>
              );
            }
            
            async function SortedMenu({ searchParams }: Pick<PageProps<"/teas">, "searchParams">) {
              const { sort = "name" } = await searchParams;
              return <Menu sort={String(sort)} />;
            }
            
            export default function TeasPage({ searchParams }: PageProps<"/teas">) {
              return (
                <main>
                  <h1>Our teas</h1>
                  <Suspense fallback={<p>Loading the menu…</p>}>
                    <SortedMenu searchParams={searchParams} />
                  </Suspense>
                </main>
              );
            }
            

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

              Check your understanding

              For each question, ask what goes into the key, and whether it can be stored.

              The 'use cache' quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Three visits call the cached getTea with "mint", "mint" and "green". How many times does getTea's body run?

                Choose an answer to see the explanation.

              2. Question 2 of 8getDetails is defined inside TeaDetails and uses tea from it. What is in its cache key?

                Read the code, then predictTSX
                import { Suspense } from "react";
                
                const api = process.env.SUPPLIER_API_URL;
                
                async function TeaDetails({ searchParams }: Pick<PageProps<"/teas">, "searchParams">) {
                  const { tea = "green" } = await searchParams;
                
                  async function getDetails() {
                    "use cache";
                    const response = await fetch(`${api}/teas/${tea}`);
                    return response.json();
                  }
                
                  const details = await getDetails();
                  return (
                    <p>
                      {details.name}: ${details.price}
                    </p>
                  );
                }
                
                export default function TeasPage({ searchParams }: PageProps<"/teas">) {
                  return (
                    <main>
                      <h1>Our teas</h1>
                      <Suspense fallback={<p>Finding the tea…</p>}>
                        <TeaDetails searchParams={searchParams} />
                      </Suspense>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 8What happens when this cached function runs?

                Read the code, then predictTypeScript
                const api = process.env.SUPPLIER_API_URL;
                
                export async function getTea(id: string, format: (name: string) => string) {
                  "use cache";
                  const response = await fetch(`${api}/teas/${id}`);
                  const tea = await response.json();
                  return { ...tea, name: format(tea.name) };
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 8Why does the build stop on this cached function?

                Read the code, then predictTypeScript
                import { cookies } from "next/headers";
                
                const api = process.env.SUPPLIER_API_URL;
                
                export async function getFavorite() {
                  "use cache";
                  const id = (await cookies()).get("favorite")?.value ?? "green";
                  const response = await fetch(`${api}/teas/${id}`);
                  return response.json();
                }
                

                Choose an answer to see the explanation.

              5. Question 5 of 8A cached TeaCard gets children. Asha's visit passes <p>Picked for you, Asha.</p> and Ben's passes the same with Ben. How many entries does TeaCard get?

                Choose an answer to see the explanation.

              6. Question 6 of 8Where does a file-level directive go, and what does it cache?

                Choose an answer to see the explanation.

              7. Question 7 of 8This page passes text as the cached TeaCard's children. Asha and Ben both visit. How many times is /teas/mint fetched?

                Read the code, then predictTSX
                import { Suspense } from "react";
                import { cookies } from "next/headers";
                import { TeaCard } from "@/app/tea-card";
                
                async function MintForYou() {
                  const name = (await cookies()).get("name")?.value ?? "friend";
                  return (
                    <TeaCard id="mint">{`Picked for you, ${name}.`}</TeaCard>
                  );
                }
                
                export default function TeasPage() {
                  return (
                    <main>
                      <h1>Our teas</h1>
                      <Suspense fallback={<p>Picking a tea…</p>}>
                        <MintForYou />
                      </Suspense>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              8. Question 8 of 8What's wrong with this function?

                Read the code, then predictTypeScript
                const api = process.env.SUPPLIER_API_URL;
                
                export function getTea(id: string) {
                  "use cache";
                  return fetch(`${api}/teas/${id}`).then((response) => response.json());
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • "use cache" goes at the top of an async function, a component, or a file. In a file, every exported function is cached and must be async.
              • The cache key is the build's ID, the function's ID, the arguments, and the values the function captures. Same key, same stored copy.
              • Arguments and results must be serializable. Class instances and functions can only pass through, and request data is read outside the cache and passed in.
              • JSX children pass through a cached component: not in the key, filled in for each request. Text children join the key.
              • Data-level caching shares one entry between every component that reads it; UI-level caching stores one rendering for each set of props.
              • Without cacheLife, the default lifetime applies: revalidate is 15 minutes, and in this version the in-memory store drops a copy after that. During the build, copies go into the prerendered output of the routes that use them; in next start, into the server's memory.

              Remember the one-liner.
              Same build, same function, same inputs: same stored copy. Keep the inputs plain, and let JSX pass through.

              Read it in Next.js's own code

              A cached call goes through cache() in use-cache-wrapper.ts, which builds the key at line 2291. The default store is createDefaultCacheHandler, an in-memory store, at lines 50 to 64 of default.ts, and its 50 MB limit is cacheMaxMemorySize at line 2304 of config-shared.ts, all at release v16.4.3.

              Coming next: Cache lifetimes and tags. cacheLife decides how long a stored copy stays fresh, and cacheTag names it, so you can expire it later.

              CompleteFrontend Clear concepts. Working examples.