$npx -y skills add vercel-labs/openreview --skill next-cache-componentsNext.js 16 Cache Components - PPR, use cache directive, cacheLife, cacheTag, updateTag
| 1 | # Cache Components (Next.js 16+) |
| 2 | |
| 3 | Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route. |
| 4 | |
| 5 | ## Enable Cache Components |
| 6 | |
| 7 | ```ts |
| 8 | // next.config.ts |
| 9 | import type { NextConfig } from 'next' |
| 10 | |
| 11 | const nextConfig: NextConfig = { |
| 12 | cacheComponents: true, |
| 13 | } |
| 14 | |
| 15 | export default nextConfig |
| 16 | ``` |
| 17 | |
| 18 | This replaces the old `experimental.ppr` flag. |
| 19 | |
| 20 | --- |
| 21 | |
| 22 | ## Three Content Types |
| 23 | |
| 24 | With Cache Components enabled, content falls into three categories: |
| 25 | |
| 26 | ### 1. Static (Auto-Prerendered) |
| 27 | |
| 28 | Synchronous code, imports, pure computations - prerendered at build time: |
| 29 | |
| 30 | ```tsx |
| 31 | export default function Page() { |
| 32 | return ( |
| 33 | <header> |
| 34 | <h1>Our Blog</h1> {/* Static - instant */} |
| 35 | <nav>...</nav> |
| 36 | </header> |
| 37 | ) |
| 38 | } |
| 39 | ``` |
| 40 | |
| 41 | ### 2. Cached (`use cache`) |
| 42 | |
| 43 | Async data that doesn't need fresh fetches every request: |
| 44 | |
| 45 | ```tsx |
| 46 | async function BlogPosts() { |
| 47 | 'use cache' |
| 48 | cacheLife('hours') |
| 49 | |
| 50 | const posts = await db.posts.findMany() |
| 51 | return <PostList posts={posts} /> |
| 52 | } |
| 53 | ``` |
| 54 | |
| 55 | ### 3. Dynamic (Suspense) |
| 56 | |
| 57 | Runtime data that must be fresh - wrap in Suspense: |
| 58 | |
| 59 | ```tsx |
| 60 | import { Suspense } from 'react' |
| 61 | |
| 62 | export default function Page() { |
| 63 | return ( |
| 64 | <> |
| 65 | <BlogPosts /> {/* Cached */} |
| 66 | |
| 67 | <Suspense fallback={<p>Loading...</p>}> |
| 68 | <UserPreferences /> {/* Dynamic - streams in */} |
| 69 | </Suspense> |
| 70 | </> |
| 71 | ) |
| 72 | } |
| 73 | |
| 74 | async function UserPreferences() { |
| 75 | const theme = (await cookies()).get('theme')?.value |
| 76 | return <p>Theme: {theme}</p> |
| 77 | } |
| 78 | ``` |
| 79 | |
| 80 | --- |
| 81 | |
| 82 | ## `use cache` Directive |
| 83 | |
| 84 | ### File Level |
| 85 | |
| 86 | ```tsx |
| 87 | 'use cache' |
| 88 | |
| 89 | export default async function Page() { |
| 90 | // Entire page is cached |
| 91 | const data = await fetchData() |
| 92 | return <div>{data}</div> |
| 93 | } |
| 94 | ``` |
| 95 | |
| 96 | ### Component Level |
| 97 | |
| 98 | ```tsx |
| 99 | export async function CachedComponent() { |
| 100 | 'use cache' |
| 101 | const data = await fetchData() |
| 102 | return <div>{data}</div> |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | ### Function Level |
| 107 | |
| 108 | ```tsx |
| 109 | export async function getData() { |
| 110 | 'use cache' |
| 111 | return db.query('SELECT * FROM posts') |
| 112 | } |
| 113 | ``` |
| 114 | |
| 115 | --- |
| 116 | |
| 117 | ## Cache Profiles |
| 118 | |
| 119 | ### Built-in Profiles |
| 120 | |
| 121 | ```tsx |
| 122 | 'use cache' // Default: 5m stale, 15m revalidate |
| 123 | ``` |
| 124 | |
| 125 | ```tsx |
| 126 | 'use cache: remote' // Platform-provided cache (Redis, KV) |
| 127 | ``` |
| 128 | |
| 129 | ```tsx |
| 130 | 'use cache: private' // For compliance, allows runtime APIs |
| 131 | ``` |
| 132 | |
| 133 | ### `cacheLife()` - Custom Lifetime |
| 134 | |
| 135 | ```tsx |
| 136 | import { cacheLife } from 'next/cache' |
| 137 | |
| 138 | async function getData() { |
| 139 | 'use cache' |
| 140 | cacheLife('hours') // Built-in profile |
| 141 | return fetch('/api/data') |
| 142 | } |
| 143 | ``` |
| 144 | |
| 145 | Built-in profiles: `'default'`, `'minutes'`, `'hours'`, `'days'`, `'weeks'`, `'max'` |
| 146 | |
| 147 | ### Inline Configuration |
| 148 | |
| 149 | ```tsx |
| 150 | async function getData() { |
| 151 | 'use cache' |
| 152 | cacheLife({ |
| 153 | stale: 3600, // 1 hour - serve stale while revalidating |
| 154 | revalidate: 7200, // 2 hours - background revalidation interval |
| 155 | expire: 86400, // 1 day - hard expiration |
| 156 | }) |
| 157 | return fetch('/api/data') |
| 158 | } |
| 159 | ``` |
| 160 | |
| 161 | --- |
| 162 | |
| 163 | ## Cache Invalidation |
| 164 | |
| 165 | ### `cacheTag()` - Tag Cached Content |
| 166 | |
| 167 | ```tsx |
| 168 | import { cacheTag } from 'next/cache' |
| 169 | |
| 170 | async function getProducts() { |
| 171 | 'use cache' |
| 172 | cacheTag('products') |
| 173 | return db.products.findMany() |
| 174 | } |
| 175 | |
| 176 | async function getProduct(id: string) { |
| 177 | 'use cache' |
| 178 | cacheTag('products', `product-${id}`) |
| 179 | return db.products.findUnique({ where: { id } }) |
| 180 | } |
| 181 | ``` |
| 182 | |
| 183 | ### `updateTag()` - Immediate Invalidation |
| 184 | |
| 185 | Use when you need the cache refreshed within the same request: |
| 186 | |
| 187 | ```tsx |
| 188 | 'use server' |
| 189 | |
| 190 | import { updateTag } from 'next/cache' |
| 191 | |
| 192 | export async function updateProduct(id: string, data: FormData) { |
| 193 | await db.products.update({ where: { id }, data }) |
| 194 | updateTag(`product-${id}`) // Immediate - same request sees fresh data |
| 195 | } |
| 196 | ``` |
| 197 | |
| 198 | ### `revalidateTag()` - Background Revalidation |
| 199 | |
| 200 | Use for stale-while-revalidate behavior: |
| 201 | |
| 202 | ```tsx |
| 203 | 'use server' |
| 204 | |
| 205 | import { revalidateTag } from 'next/cache' |
| 206 | |
| 207 | export async function createPost(data: FormData) { |
| 208 | await db.posts.create({ data }) |
| 209 | revalidateTag('posts') // Background - next request sees fresh data |
| 210 | } |
| 211 | ``` |
| 212 | |
| 213 | --- |
| 214 | |
| 215 | ## Runtime Data Constraint |
| 216 | |
| 217 | **Cannot** access `cookies()`, `headers()`, or `searchParams` inside `use cache`. |
| 218 | |
| 219 | ### Solution: Pass as Arguments |
| 220 | |
| 221 | ```tsx |
| 222 | // Wrong - runtime API inside use cache |
| 223 | async function CachedProfile() { |
| 224 | 'use cache' |
| 225 | const session = (await cookies()).get('session')?.value // Error! |
| 226 | return <div>{session}</div> |
| 227 | } |
| 228 | |
| 229 | // Correct - extract outside, pass as argument |
| 230 | async function ProfilePage() { |
| 231 | const session = (await cookies()).get('session')?.value |
| 232 | return <CachedProfile sessionId={session} /> |
| 233 | } |
| 234 | |
| 235 | async function CachedProfile({ sessionId }: { sessionId: string }) { |
| 236 | 'use cache' |
| 237 | // sessionId becomes part of cache key automatically |
| 238 | const data = await fetchUserData(sessionId) |
| 239 | return <div>{data.name}</div> |
| 240 | } |
| 241 | ``` |
| 242 | |
| 243 | ### Exception: `use cache: private` |
| 244 | |
| 245 | For compliance requirements when you can't refactor: |
| 246 | |
| 247 | ```tsx |
| 248 | async function getData() { |
| 249 | 'use cache: private' |
| 250 | const session = (await cookies()).get('session')?.value // Allowed |
| 251 | return fe |