Next.js Cache Components
Auto-activation: This skill activates automatically in projects with cacheComponents: true in next.config.
Project Detection
When starting work in a Next.js project, check if Cache Components are enabled:
Check next.config.ts or next.config.js for cacheComponents
grep -r "cacheComponents" next.config.* 2>/dev/null
If cacheComponents: true is found, apply this skill's patterns proactively when:
-
Writing React Server Components
-
Implementing data fetching
-
Creating Server Actions with mutations
-
Optimizing page performance
-
Reviewing existing component code
Cache Components enable Partial Prerendering (PPR) - mixing static HTML shells with dynamic streaming content for optimal performance.
Philosophy: Code Over Configuration
Cache Components represents a shift from segment configuration to compositional code:
Before (Deprecated) After (Cache Components)
export const revalidate = 3600
cacheLife('hours') inside 'use cache'
export const dynamic = 'force-static'
Use 'use cache' and Suspense boundaries
All-or-nothing static/dynamic Granular: static shell + cached + dynamic
Key Principle: Components co-locate their caching, not just their data. Next.js provides build-time feedback to guide you toward optimal patterns.
Core Concept
┌─────────────────────────────────────────────────────┐ │ Static Shell │ │ (Sent immediately to browser) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Header │ │ Cached │ │ Suspense │ │ │ │ (static) │ │ Content │ │ Fallback │ │ │ └─────────────┘ └─────────────┘ └──────┬──────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ Dynamic │ │ │ │ (streams) │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────┘
Mental Model: The Caching Decision Tree
When writing a React Server Component, ask these questions in order:
┌─────────────────────────────────────────────────────────┐ │ Does this component fetch data or perform I/O? │ └─────────────────────┬───────────────────────────────────┘ │ ┌──────────▼──────────┐ │ YES │ NO → Pure component, no action needed └──────────┬──────────┘ │ ┌─────────────────▼─────────────────┐ │ Does it depend on request context? │ │ (cookies, headers, searchParams) │ └─────────────────┬─────────────────┘ │ ┌────────────┴────────────┐ │ │ ┌────▼────┐ ┌─────▼─────┐ │ YES │ │ NO │ └────┬────┘ └─────┬─────┘ │ │ │ ┌─────▼─────────────────┐ │ │ Can this be cached? │ │ │ (same for all users?) │ │ └─────┬─────────────────┘ │ │ │ ┌──────────┴──────────┐ │ │ │ │ ┌────▼────┐ ┌─────▼─────┐ │ │ YES │ │ NO │ │ └────┬────┘ └─────┬─────┘ │ │ │ │ ▼ │ │ 'use cache' │ │ + cacheTag() │ │ │ └──────────────┬─────────────────────┘ │ ▼ Wrap in <Suspense> (dynamic streaming)
Key insight: The 'use cache' directive is for data that's the same across users. User-specific data stays dynamic with Suspense.
Quick Start
Enable Cache Components
// next.config.ts import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true, };
export default nextConfig;
Basic Usage
// Cached component - output included in static shell async function CachedPosts() { "use cache"; const posts = await db.posts.findMany(); return <PostList posts={posts} />; }
// Page with static + cached + dynamic content export default async function BlogPage() { return ( <> <Header /> {/* Static /} <CachedPosts /> {/ Cached /} <Suspense fallback={<Skeleton />}> <DynamicComments /> {/ Dynamic - streams */} </Suspense> </> ); }
Core APIs
- 'use cache' Directive
Marks code as cacheable. Can be applied at three levels:
// File-level: All exports are cached "use cache"; export async function getData() { /* ... / } export async function Component() { / ... */ }
// Component-level async function UserCard({ id }: { id: string }) { "use cache"; const user = await fetchUser(id); return <Card>{user.name}</Card>; }
// Function-level async function fetchWithCache(url: string) { "use cache"; return fetch(url).then((r) => r.json()); }
Important: All cached functions must be async .
- cacheLife()
- Control Cache Duration
import { cacheLife } from "next/cache";
async function Posts() { "use cache"; cacheLife("hours"); // Use a predefined profile
// Or custom configuration: cacheLife({ stale: 60, // 1 min - client cache validity revalidate: 3600, // 1 hr - start background refresh expire: 86400, // 1 day - absolute expiration });
return await db.posts.findMany(); }
Predefined profiles: 'default' , 'seconds' , 'minutes' , 'hours' , 'days' , 'weeks' , 'max'
- cacheTag()
- Tag for Invalidation
import { cacheTag } from "next/cache";
async function BlogPosts() { "use cache"; cacheTag("posts"); cacheLife("days");
return await db.posts.findMany(); }
async function UserProfile({ userId }: { userId: string }) {
"use cache";
cacheTag("users", user-${userId}); // Multiple tags
return await db.users.findUnique({ where: { id: userId } }); }
- updateTag()
- Immediate Invalidation
For read-your-own-writes semantics:
"use server"; import { updateTag } from "next/cache";
export async function createPost(formData: FormData) { await db.posts.create({ data: formData });
updateTag("posts"); // Client immediately sees fresh data }
- revalidateTag()
- Background Revalidation
For stale-while-revalidate pattern:
"use server"; import { revalidateTag } from "next/cache";
export async function updatePost(id: string, data: FormData) { await db.posts.update({ where: { id }, data });
revalidateTag("posts", "max"); // Serve stale, refresh in background }
When to Use Each Pattern
Content Type API Behavior
Static No directive Rendered at build time
Cached 'use cache'
Included in static shell, revalidates
Dynamic Inside <Suspense>
Streams at request time
Parameter Permutations & Subshells
Critical Concept: With Cache Components, Next.js renders ALL permutations of provided parameters to create reusable subshells.
// app/products/[category]/[slug]/page.tsx export async function generateStaticParams() { return [ { category: "jackets", slug: "classic-bomber" }, { category: "jackets", slug: "essential-windbreaker" }, { category: "accessories", slug: "thermal-fleece-gloves" }, ]; }
Next.js renders these routes:
/products/jackets/classic-bomber ← Full params (complete page) /products/jackets/essential-windbreaker ← Full params (complete page) /products/accessories/thermal-fleece-gloves ← Full params (complete page) /products/jackets/[slug] ← Partial params (category subshell) /products/accessories/[slug] ← Partial params (category subshell) /products/[category]/[slug] ← No params (fallback shell)
Why this matters: The category subshell (/products/jackets/[slug] ) can be reused for ANY jacket product, even ones not in generateStaticParams . Users navigating to an unlisted jacket get the cached category shell immediately, with product details streaming in.
generateStaticParams Requirements
With Cache Components enabled:
-
Must provide at least one parameter - Empty arrays now cause build errors (prevents silent production failures)
-
Params prove static safety - Providing params lets Next.js verify no dynamic APIs are called
-
Partial params create subshells - Each unique permutation generates a reusable shell
// ❌ ERROR with Cache Components export function generateStaticParams() { return []; // Build error: must provide at least one param }
// ✅ CORRECT: Provide real params export async function generateStaticParams() { const products = await getPopularProducts(); return products.map(({ category, slug }) => ({ category, slug })); }
Cache Key = Arguments
Arguments become part of the cache key:
// Different userId = different cache entry
async function UserData({ userId }: { userId: string }) {
"use cache";
cacheTag(user-${userId});
return await fetchUser(userId); }
Build-Time Feedback
Cache Components provides early feedback during development. These build errors guide you toward optimal patterns:
Error: Dynamic data outside Suspense
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
Solution: Wrap dynamic components in <Suspense> :
<Suspense fallback={<Skeleton />}> <ComponentThatUsesCookies /> </Suspense>
Error: Uncached data outside Suspense
Error: Accessing uncached data outside Suspense
Solution: Either cache the data or wrap in Suspense:
// Option 1: Cache it async function ProductData({ id }: { id: string }) { "use cache"; return await db.products.findUnique({ where: { id } }); }
// Option 2: Make it dynamic with Suspense <Suspense fallback={<Loading />}> <DynamicProductData id={id} /> </Suspense>;
Error: Request data inside cache
Error: Cannot access cookies/headers inside 'use cache'
Solution: Extract runtime data outside cache boundary (see "Handling Runtime Data" above).
Additional Resources
-
For complete API reference, see REFERENCE.md
-
For common patterns and recipes, see PATTERNS.md
-
For debugging and troubleshooting, see TROUBLESHOOTING.md
Code Generation Guidelines
When generating Cache Component code:
-
Always use async
-
All cached functions must be async
-
Place 'use cache' first - Must be first statement in function body
-
Call cacheLife() early - Should follow 'use cache' directive
-
Tag meaningfully - Use semantic tags that match your invalidation needs
-
Extract runtime data - Move cookies() /headers() outside cached scope
-
Wrap dynamic content - Use <Suspense> for non-cached async components
Proactive Application (When Cache Components Enabled)
When cacheComponents: true is detected in the project, automatically apply these patterns:
When Writing Data Fetching Components
Ask yourself: "Can this data be cached?" If yes, add 'use cache' :
// Before: Uncached fetch async function ProductList() { const products = await db.products.findMany(); return <Grid products={products} />; }
// After: With caching async function ProductList() { "use cache"; cacheTag("products"); cacheLife("hours");
const products = await db.products.findMany(); return <Grid products={products} />; }
When Writing Server Actions
Always invalidate relevant caches after mutations:
"use server"; import { updateTag } from "next/cache";
export async function createProduct(data: FormData) { await db.products.create({ data }); updateTag("products"); // Don't forget! }
When Composing Pages
Structure with static shell + cached content + dynamic streaming:
export default async function Page() { return ( <> <StaticHeader /> {/* No cache needed /} <CachedContent /> {/ 'use cache' /} <Suspense fallback={<Skeleton />}> <DynamicUserContent /> {/ Streams at runtime */} </Suspense> </> ); }
When Reviewing Code
Flag these issues in Cache Components projects:
-
Data fetching without 'use cache' where caching would benefit
-
Missing cacheTag() calls (makes invalidation impossible)
-
Server Actions without updateTag() /revalidateTag() after mutations
-
cookies() /headers() called inside 'use cache' scope
-
Dynamic components without <Suspense> boundaries
-
DEPRECATED: export const revalidate
-
replace with cacheLife() in 'use cache'
-
DEPRECATED: export const dynamic
-
replace with Suspense + cache boundaries
-
Empty generateStaticParams() return - must provide at least one param
Note
This project use a default cacheLife set to the max profile, so you don't need to call cacheLife() in most cases unless you need to customize the cache lifetime.