Next.js 'use cache': Explicit Caching with Automatic Keys
Cache pages, components, and functions with opt-in directive and stale-while-revalidate profiles
Next.js 16 introduces "use cache"—an explicit directive that caches pages, components, or functions with automatic cache key generation. Unlike previous implicit caching, all dynamic code runs at request time by default. You opt-in to caching where performance matters.
The Caching Inversion
Next.js 15 and earlier aggressively cached everything unless you explicitly opted out with export const dynamic = 'force-dynamic'. This caused confusion when data didn't update as expected. Next.js 16 inverts this:
// Without "use cache": Runs on every request
export default async function BlogPage() {
const posts = await db.posts.findAll(); // Fresh query each time
return <PostList posts={posts} />;
}
// With "use cache": Cached with automatic key generation
'use cache';
export default async function BlogPage() {
const posts = await db.posts.findAll(); // Cached result
return <PostList posts={posts} />;
}Cache Scope: Three Levels
1. Page-Level Caching
// app/blog/page.tsx
'use cache';
export const cacheLife = 'max'; // Use predefined profile
export default async function BlogPage() {
const posts = await db.posts.findAll();
return <PostList posts={posts} />;
}Entire page output is cached. Equivalent to static site generation but with runtime control.
2. Component-Level Caching
// components/UserProfile.tsx
'use cache';
export const cacheLife = 'default';
export async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // Cached per userId
return (
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}Cache key automatically includes userId prop. Each unique userId gets its own cache entry. This completes Partial Pre-Rendering (PPR)—cache expensive components while keeping others dynamic.
3. Function-Level Caching
'use cache';
export const cacheLife = {
stale: 900, // 15 minutes
revalidate: 86400, // 1 day
expire: 604800 // 7 days (hard expiration)
};
export async function getPopularPosts(category: string) {
const posts = await db.posts.findPopular(category);
return posts;
}
// Usage (in any component)
const posts = await getPopularPosts('nextjs'); // Cached per categoryCache Keys: Automatic Generation
Next.js generates cache keys from function arguments and component props. No manual key management:
'use cache';
async function fetchProduct(id: string, locale: string) {
// Cache key: "fetchProduct:id=123:locale=en"
return await db.products.findOne({ id, locale });
}
// Different arguments = different cache entries
await fetchProduct('123', 'en'); // Cache miss → DB query
await fetchProduct('123', 'fr'); // Cache miss → DB query
await fetchProduct('123', 'en'); // Cache hit → instantcacheLife Profiles
Predefined profiles for common caching strategies:
export const cacheLife = 'default';
// stale: 900s (15min), revalidate: 86400s (1 day), expire: infinite
export const cacheLife = 'max';
// stale: 604800s (7 days), revalidate: 31536000s (1 year), expire: infinite
export const cacheLife = 'minutes';
// stale: 60s, revalidate: 600s (10min), expire: 3600s (1 hour)Custom profiles in next.config.ts:
// next.config.ts
const nextConfig = {
cacheLife: {
blog: {
stale: 3600, // 1 hour stale
revalidate: 86400, // Revalidate daily
expire: 604800 // Hard expire after 7 days
}
}
};
// Usage
'use cache';
export const cacheLife = 'blog';Stale-While-Revalidate Behavior
Cache lifecycle with cacheLife = 'default':
- 0–15 min: Serve cached version instantly (fresh)
- 15 min–1 day: Serve stale cached version + trigger background revalidation
- After 1 day: Cache expires, next request generates fresh version and caches it
'use cache';
export const cacheLife = 'default';
export async function NewsWidget() {
const news = await fetch('https://api.news.com/latest');
// t=0min: Cache miss → Fetch and cache
// t=10min: Cache hit → Instant response (fresh)
// t=20min: Cache hit → Instant stale response + revalidate in background
// t=2days: Cache expired → Fresh fetch and cache
return <NewsList items={news} />;
}Edge Cases and Gotchas
Cache Keys Include All Props
Objects and arrays in props affect cache keys. Identical data with different object references = cache miss:
'use cache';
function ProductCard({ filters }: { filters: { category: string } }) {
// Cache key includes filters object structure
}
// Cache miss (different object references)
<ProductCard filters={{ category: 'electronics' }} />
<ProductCard filters={{ category: 'electronics' }} />
// Solution: Stable references
const filters = { category: 'electronics' };
<ProductCard filters={filters} />
<ProductCard filters={filters} /> // Cache hitNo Caching for Mutations
"use cache" only works with read operations. Server Actions and mutations cannot be cached:
// ❌ Error: Server Actions cannot use "use cache"
'use cache';
export async function updateUser(formData: FormData) {
await db.users.update(formData);
}
// ✓ Correct: Only cache reads
'use cache';
export async function getUser(id: string) {
return await db.users.findById(id);
}Request Context Breaks Caching
Accessing request-specific APIs (cookies(), headers()) inside cached functions disables caching:
'use cache'; // Has no effect due to cookies() call
export async function UserGreeting() {
const cookieStore = await cookies();
const name = cookieStore.get('username');
return <h1>Hello {name}</h1>;
}
// Solution: Move request context out of cached boundary
export async function UserGreeting() {
const cookieStore = await cookies();
const name = cookieStore.get('username');
return <CachedGreeting name={name} />;
}
'use cache';
async function CachedGreeting({ name }: { name: string }) {
return <h1>Hello {name}</h1>;
}Revalidation Strategies
On-Demand: revalidateTag()
import { revalidateTag } from 'next/cache';
'use cache';
export const cacheLife = 'max';
export const cacheTags = ['blog-posts'];
export async function BlogList() {
const posts = await db.posts.findAll();
return <PostList posts={posts} />;
}
// Trigger revalidation from Server Action
export async function publishPost(formData: FormData) {
await db.posts.create(formData);
await revalidateTag('blog-posts', 'max'); // Requires cacheLife profile
}Read-Your-Writes: updateTag()
New API that expires cache and immediately fetches fresh data in the same request:
import { updateTag } from 'next/cache';
export async function saveUserSettings(formData: FormData) {
await db.users.updateSettings(formData);
// Expire cache + fetch fresh data immediately (no stale response)
await updateTag('user-settings');
// User sees updated data without page refresh
}Migration from Next.js 15
// Next.js 15: Implicitly cached
export default async function Page() {
const posts = await fetch('https://api.example.com/posts');
return <PostList posts={posts} />;
}
// Next.js 16: Add "use cache" to maintain caching
'use cache';
export const cacheLife = 'default';
export default async function Page() {
const posts = await fetch('https://api.example.com/posts');
return <PostList posts={posts} />;
}Finding Pages That Need Caching
- Run
next buildand check output for Dynamic vs Static routes - Add
"use cache"to routes that should be static - Monitor response times in production—add caching where latency spikes
Configuration
// next.config.ts
const nextConfig = {
// Enable Cache Components (required for component/function caching)
cacheComponents: true,
// Custom cache profiles
cacheLife: {
frequent: {
stale: 60, // 1 minute
revalidate: 900, // 15 minutes
expire: 3600 // 1 hour
},
rare: {
stale: 86400, // 1 day
revalidate: 604800, // 7 days
expire: 2592000 // 30 days
}
}
};
export default nextConfig;Cache Components shift caching from framework magic to developer intent. If data should be cached, say so explicitly. If it should be fresh, don't add the directive. Predictable, debuggable, and aligned with how caching works in every other framework.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Related Insights
Explore related edge cases and patterns
Advertisement