Lesson 10 of 81
Instead of a route being either fully static or fully dynamic, Cache Components lets you cache one function inside an otherwise-dynamic page — the static/dynamic boundary moves from the route down to individual functions and components.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A cached function getProductsByCategory(category) is called once with 'shoes' and once with 'bags'. What's true about the two calls?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Cache Components is a rendering model, enabled with cacheComponents: true in next.config.ts, where static and dynamic content can coexist inside a single route instead of the whole route being classified one way. The mechanism is the "use cache" directive: add it to the top of an async function, an async component, or a whole file, and its return value gets cached.
async — "use cache" only works on async code.cacheLife() for time-based revalidation (built-in profiles like 'hours', 'days', 'max') and cacheTag() + revalidateTag/updateTag for on-demand invalidation after a mutation.cookies(), headers(), or read searchParams directly — those are Request-time APIs, and a cached function's result has to be shareable across requests, which reading request-specific data would break. Read them outside the cached scope and pass the extracted value in as an argument instead.// app/lib/products.ts
import { cacheLife, cacheTag } from "next/cache";
export async function getFeaturedProducts() {
"use cache";
cacheLife("hours");
cacheTag("products");
const res = await fetch("https://api.example.com/products/featured");
return res.json();
}
// app/page.tsx
import { cookies } from "next/headers";
import { getFeaturedProducts } from "./lib/products";
export default async function HomePage() {
const products = await getFeaturedProducts(); // cached, shared across visitors
// Request-time data read OUTSIDE the cached scope, then passed as an argument
const region = (await cookies()).get("region")?.value ?? "us";
return <ProductGrid products={products} region={region} />;
}
getProducts('shoes') is called — no cache entry exists yet, so it executes and stores the result.
getFeaturedProducts() runs once, and every visitor within its cacheLife window gets the same cached result — the function's arguments (none, here) and closure are part of what makes its cache key, so calling it again with the same inputs is a cache hit, not a re-execution. region, on the other hand, has to come from cookies() outside any cached scope, because reading it directly inside getFeaturedProducts would throw: cached functions can't touch request-time data.
"use cache" requires cacheComponents: true in next.config.ts — it's not a standalone primitive you can drop in anywhere. It's part of the same feature, not a separate opt-in.async, even if it doesn't currently await anything. This is a hard requirement of the directive, not a style preference.cookies(), headers(), or reading searchParams inside a "use cache" scope is an error, not a silent fallback to uncached behavior. The fix is always the same: read the value outside the cached function, then pass it in as an argument so it becomes part of the cache key.userId variable gets a separate cache entry per userId, even though userId was never passed explicitly as a parameter.cacheLife() doesn't mean "never expires" — it means the implicit default profile applies, which does have a real revalidation window. Pairing every "use cache" with an explicit cacheLife() call is the recommended practice specifically so the lifetime is visible at the call site instead of hidden behind a default."How would you cache one expensive database query on a page that also needs to show the logged-in user's name?" tests whether a candidate reaches for function-level caching with an explicit argument boundary, or tries to cache the whole page and then gets stuck when cookies() throws inside it — the request-time-API constraint is exactly the trap that reveals real hands-on experience with the model.