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.
- 01Place the directiveCache a function, a component or a whole file, and know the rules for each: async, and every export in a cached file.
- 02Predict the cache keySay which calls share one stored copy: the same build, the same function, and the same arguments and captured values.
- 03Keep 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.
lib/supplier.tsconst 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();}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.
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.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
"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.
lib/supplier.tsconst 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();}Mint tea: $3
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
Nothing is cached. Over the three visits, the supplier was asked for /teas/mint twice and /teas/green once: one request for each visit.
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:
| Part | What it is | What gives a new key |
|---|---|---|
| The build's ID | A 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 ID | A 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 arguments | What the call passes in: props for a component, arguments for a function. | Any different value: "mint" and "green" are two entries. |
| Captured values | Values 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:
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:
app/teas/page.tsximport { 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> );}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"):
app/page.tsximport { 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> );}$ 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 contentThe 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:
| Value | As an argument | As the result |
|---|---|---|
Text, numbers, true, false, null, undefined | Yes, and they become part of the key. | Yes. |
| Plain objects and arrays of those | Yes. | Yes: getTea returns the object it got from JSON. |
Date, Map, Set | Yes, 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. |
| Functions | Only passed through: calling one fails. | Only one passed through, like a Server Action handed on to a Client Component. |
| Instances of a class | Only passed through: reading one fails. | No: the result can't be stored. |
lib/supplier.tsNext.jslib/supplier.tsconst 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();}$ 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 contentgetTea gets a string and returns plain data from JSON. Both can be stored: the visit shows Mint tea: $3.
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
TeaIdarrived as a reference that can only be passed through, so readingid.valuefailed 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
formatfailed. - 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
Teafailed.
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:
lib/supplier.tsimport { 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▲ 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: nullLine 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:
app/tea-card.tsximport 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> );}$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.
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:
app/teas/page.tsxNext.jsapp/teas/page.tsximport { 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> );}$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.
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.
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:
app/teas/page.tsxNext.jsapp/teas/page.tsximport { 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> );}- 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.
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.
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:
| Question | Data: the directive on a data function | UI: the directive on a component |
|---|---|---|
| What's stored | The data the function returns, like the menu's list. | What the component rendered: its output for React. |
| One entry for each | Set of arguments. getMenu() has none, so one entry. | Set of props. <Menu sort="price"> and <Menu sort="name"> are two. |
| Who can use it | Any component that calls the function, rendering it its own way. | Only that component, with those props. |
| Good for | Data many parts of the app read. | A part whose rendering is the work, with few prop values. |
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:
getTea("mint"): a string argument.- A cached inner function that uses
teafrom the function around it. - A cached component that returns JSX.
- Passing
<p>Picked for you, Asha.</p>as children. getTea(new TeaId("mint")), then readingid.valueinside.- 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'tasync.
Sort each use of a cached function or component by what Next.js 16.4.3 did with it.
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.
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?
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();
}
Two. "mint" and "green" are two keys, and the second mint visit used the copy the first one stored. The recording shows /teas/mint once and /teas/green once.
The build stops at line 3 of this lib/supplier.ts. What's missing?
const api = process.env.SUPPLIER_API_URL;
export function getTea(id: string) {
"use cache";
return fetch(`${api}/teas/${id}`).then((response) => response.json());
}
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();
}
async. A cached function must be declared async, even if it already returns a promise. With export async function getTea, the build passes, as in the overview's recording.
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?
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();
}
import { Suspense } from "react";
import { cookies } from "next/headers";
import { getTea } from "@/lib/supplier";
async function Favorite() {
const id = (await cookies()).get("favorite")?.value ?? "green";
const tea = await getTea(id);
return <p>Your favorite: {tea.name}</p>;
}
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Suspense fallback={<p>Finding your favorite…</p>}>
<Favorite />
</Suspense>
</main>
);
}
Once. Favorite reads the cookie on line 6, outside the cache and inside Suspense, and passes the id to the cached getTea. Asha and Ben, both with favorite=mint, shared one copy: the recording shows /teas/mint once.
A visit fails with an error that starts Cannot access value on the server, because getTea takes a TeaId. What should it take instead?
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();
}
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();
}
A string: the tea's id. Strings are stored as values and join the key. With getTea(id: string), the visit shows Mint tea: $3, as in the overview's recording.
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?
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>
);
}
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>
);
}
Once. As <p>Picked for you, {name}.</p>, the greeting is JSX and passes through, so Asha and Ben share one key. The recording shows /teas/mint once, and each visitor sees their own name.
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?
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>
);
}
import { Suspense } from "react";
const api = process.env.SUPPLIER_API_URL;
type Tea = { id: string; name: string; price: number };
async function getMenu(): Promise<Tea[]> {
"use cache";
const response = await fetch(`${api}/menu`);
return response.json();
}
async function Menu({ sort }: { sort: string }) {
const teas = await getMenu();
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>
);
}
Once. getMenu on lines 7 to 11 is cached and takes no arguments, so one entry serves every order, and Menu sorts the list for each visit. The recording shows /menu once for the visits with name, price and price.
Check your understanding
For each question, ask what goes into the key, and whether it can be stored.
Question 1 of 8Three visits call the cached
getTeawith"mint","mint"and"green". How many times doesgetTea's body run?Choose an answer to see the explanation.
Question 2 of 8
getDetailsis defined insideTeaDetailsand usesteafrom it. What is in its cache key?Read the code, then predictTSXimport { 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.
Question 3 of 8What happens when this cached function runs?
Read the code, then predictTypeScriptconst 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.
Question 4 of 8Why does the build stop on this cached function?
Read the code, then predictTypeScriptimport { 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.
Question 5 of 8A cached
TeaCardgetschildren. Asha's visit passes<p>Picked for you, Asha.</p>and Ben's passes the same with Ben. How many entries doesTeaCardget?Choose an answer to see the explanation.
Question 6 of 8Where does a file-level directive go, and what does it cache?
Choose an answer to see the explanation.
Question 7 of 8This page passes text as the cached
TeaCard's children. Asha and Ben both visit. How many times is/teas/mintfetched?Read the code, then predictTSXimport { 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.
Question 8 of 8What's wrong with this function?
Read the code, then predictTypeScriptconst 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 beasync.- 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:revalidateis 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; innext 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.
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.