Next.js 16 Cache Components - PPR, use cache directive, cacheLife, cacheTag, updateTag
Add this skill
npx mdskills install vercel-labs/next-cache-componentsComprehensive Next.js 16 Cache Components reference with clear migration paths and practical examples
1---2name: next-cache-components3description: Next.js 16 Cache Components - PPR, use cache directive, cacheLife, cacheTag, updateTag4---56# Cache Components (Next.js 16+)78Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.910## Enable Cache Components1112```ts13// next.config.ts14import type { NextConfig } from 'next'1516const nextConfig: NextConfig = {17 cacheComponents: true,18}1920export default nextConfig21```2223This replaces the old `experimental.ppr` flag.2425---2627## Three Content Types2829With Cache Components enabled, content falls into three categories:3031### 1. Static (Auto-Prerendered)3233Synchronous code, imports, pure computations - prerendered at build time:3435```tsx36export default function Page() {37 return (38 <header>39 <h1>Our Blog</h1> {/* Static - instant */}40 <nav>...</nav>41 </header>42 )43}44```4546### 2. Cached (`use cache`)4748Async data that doesn't need fresh fetches every request:4950```tsx51async function BlogPosts() {52 'use cache'53 cacheLife('hours')5455 const posts = await db.posts.findMany()56 return <PostList posts={posts} />57}58```5960### 3. Dynamic (Suspense)6162Runtime data that must be fresh - wrap in Suspense:6364```tsx65import { Suspense } from 'react'6667export default function Page() {68 return (69 <>70 <BlogPosts /> {/* Cached */}7172 <Suspense fallback={<p>Loading...</p>}>73 <UserPreferences /> {/* Dynamic - streams in */}74 </Suspense>75 </>76 )77}7879async function UserPreferences() {80 const theme = (await cookies()).get('theme')?.value81 return <p>Theme: {theme}</p>82}83```8485---8687## `use cache` Directive8889### File Level9091```tsx92'use cache'9394export default async function Page() {95 // Entire page is cached96 const data = await fetchData()97 return <div>{data}</div>98}99```100101### Component Level102103```tsx104export async function CachedComponent() {105 'use cache'106 const data = await fetchData()107 return <div>{data}</div>108}109```110111### Function Level112113```tsx114export async function getData() {115 'use cache'116 return db.query('SELECT * FROM posts')117}118```119120---121122## Cache Profiles123124### Built-in Profiles125126```tsx127'use cache' // Default: 5m stale, 15m revalidate128```129130```tsx131'use cache: remote' // Platform-provided cache (Redis, KV)132```133134```tsx135'use cache: private' // For compliance, allows runtime APIs136```137138### `cacheLife()` - Custom Lifetime139140```tsx141import { cacheLife } from 'next/cache'142143async function getData() {144 'use cache'145 cacheLife('hours') // Built-in profile146 return fetch('/api/data')147}148```149150Built-in profiles: `'default'`, `'minutes'`, `'hours'`, `'days'`, `'weeks'`, `'max'`151152### Inline Configuration153154```tsx155async function getData() {156 'use cache'157 cacheLife({158 stale: 3600, // 1 hour - serve stale while revalidating159 revalidate: 7200, // 2 hours - background revalidation interval160 expire: 86400, // 1 day - hard expiration161 })162 return fetch('/api/data')163}164```165166---167168## Cache Invalidation169170### `cacheTag()` - Tag Cached Content171172```tsx173import { cacheTag } from 'next/cache'174175async function getProducts() {176 'use cache'177 cacheTag('products')178 return db.products.findMany()179}180181async function getProduct(id: string) {182 'use cache'183 cacheTag('products', `product-${id}`)184 return db.products.findUnique({ where: { id } })185}186```187188### `updateTag()` - Immediate Invalidation189190Use when you need the cache refreshed within the same request:191192```tsx193'use server'194195import { updateTag } from 'next/cache'196197export async function updateProduct(id: string, data: FormData) {198 await db.products.update({ where: { id }, data })199 updateTag(`product-${id}`) // Immediate - same request sees fresh data200}201```202203### `revalidateTag()` - Background Revalidation204205Use for stale-while-revalidate behavior:206207```tsx208'use server'209210import { revalidateTag } from 'next/cache'211212export async function createPost(data: FormData) {213 await db.posts.create({ data })214 revalidateTag('posts') // Background - next request sees fresh data215}216```217218---219220## Runtime Data Constraint221222**Cannot** access `cookies()`, `headers()`, or `searchParams` inside `use cache`.223224### Solution: Pass as Arguments225226```tsx227// Wrong - runtime API inside use cache228async function CachedProfile() {229 'use cache'230 const session = (await cookies()).get('session')?.value // Error!231 return <div>{session}</div>232}233234// Correct - extract outside, pass as argument235async function ProfilePage() {236 const session = (await cookies()).get('session')?.value237 return <CachedProfile sessionId={session} />238}239240async function CachedProfile({ sessionId }: { sessionId: string }) {241 'use cache'242 // sessionId becomes part of cache key automatically243 const data = await fetchUserData(sessionId)244 return <div>{data.name}</div>245}246```247248### Exception: `use cache: private`249250For compliance requirements when you can't refactor:251252```tsx253async function getData() {254 'use cache: private'255 const session = (await cookies()).get('session')?.value // Allowed256 return fetchData(session)257}258```259260---261262## Cache Key Generation263264Cache keys are automatic based on:265- **Build ID** - invalidates all caches on deploy266- **Function ID** - hash of function location267- **Serializable arguments** - props become part of key268- **Closure variables** - outer scope values included269270```tsx271async function Component({ userId }: { userId: string }) {272 const getData = async (filter: string) => {273 'use cache'274 // Cache key = userId (closure) + filter (argument)275 return fetch(`/api/users/${userId}?filter=${filter}`)276 }277 return getData('active')278}279```280281---282283## Complete Example284285```tsx286import { Suspense } from 'react'287import { cookies } from 'next/headers'288import { cacheLife, cacheTag } from 'next/cache'289290export default function DashboardPage() {291 return (292 <>293 {/* Static shell - instant from CDN */}294 <header><h1>Dashboard</h1></header>295 <nav>...</nav>296297 {/* Cached - fast, revalidates hourly */}298 <Stats />299300 {/* Dynamic - streams in with fresh data */}301 <Suspense fallback={<NotificationsSkeleton />}>302 <Notifications />303 </Suspense>304 </>305 )306}307308async function Stats() {309 'use cache'310 cacheLife('hours')311 cacheTag('dashboard-stats')312313 const stats = await db.stats.aggregate()314 return <StatsDisplay stats={stats} />315}316317async function Notifications() {318 const userId = (await cookies()).get('userId')?.value319 const notifications = await db.notifications.findMany({320 where: { userId, read: false }321 })322 return <NotificationList items={notifications} />323}324```325326---327328## Migration from Previous Versions329330| Old Config | Replacement |331|-----------|-------------|332| `experimental.ppr` | `cacheComponents: true` |333| `dynamic = 'force-dynamic'` | Remove (default behavior) |334| `dynamic = 'force-static'` | `'use cache'` + `cacheLife('max')` |335| `revalidate = N` | `cacheLife({ revalidate: N })` |336| `unstable_cache()` | `'use cache'` directive |337338### Migrating `unstable_cache` to `use cache`339340`unstable_cache` has been replaced by the `use cache` directive in Next.js 16. When `cacheComponents` is enabled, convert `unstable_cache` calls to `use cache` functions:341342**Before (`unstable_cache`):**343344```tsx345import { unstable_cache } from 'next/cache'346347const getCachedUser = unstable_cache(348 async (id) => getUser(id),349 ['my-app-user'],350 {351 tags: ['users'],352 revalidate: 60,353 }354)355356export default async function Page({ params }: { params: Promise<{ id: string }> }) {357 const { id } = await params358 const user = await getCachedUser(id)359 return <div>{user.name}</div>360}361```362363**After (`use cache`):**364365```tsx366import { cacheLife, cacheTag } from 'next/cache'367368async function getCachedUser(id: string) {369 'use cache'370 cacheTag('users')371 cacheLife({ revalidate: 60 })372 return getUser(id)373}374375export default async function Page({ params }: { params: Promise<{ id: string }> }) {376 const { id } = await params377 const user = await getCachedUser(id)378 return <div>{user.name}</div>379}380```381382Key differences:383- **No manual cache keys** - `use cache` generates keys automatically from function arguments and closures. The `keyParts` array from `unstable_cache` is no longer needed.384- **Tags** - Replace `options.tags` with `cacheTag()` calls inside the function.385- **Revalidation** - Replace `options.revalidate` with `cacheLife({ revalidate: N })` or a built-in profile like `cacheLife('minutes')`.386- **Dynamic data** - `unstable_cache` did not support `cookies()` or `headers()` inside the callback. The same restriction applies to `use cache`, but you can use `'use cache: private'` if needed.387388---389390## Limitations391392- **Edge runtime not supported** - requires Node.js393- **Static export not supported** - needs server394- **Non-deterministic values** (`Math.random()`, `Date.now()`) execute once at build time inside `use cache`395396For request-time randomness outside cache:397398```tsx399import { connection } from 'next/server'400401async function DynamicContent() {402 await connection() // Defer to request time403 const id = crypto.randomUUID() // Different per request404 return <div>{id}</div>405}406```407408Sources:409- [Cache Components Guide](https://nextjs.org/docs/app/getting-started/cache-components)410- [use cache Directive](https://nextjs.org/docs/app/api-reference/directives/use-cache)411- [unstable_cache (legacy)](https://nextjs.org/docs/app/api-reference/functions/unstable_cache)412
Full transparency — inspect the skill content before installing.