The "use cache" Directive
Caching eliminates in-between states entirely—if content is pre-rendered, there's nothing to wait for. Next.js 16 introduces the "use cache" directive for fine-grained, explicit caching control.
With cacheComponents: true in your Next.js config, data fetching is dynamic by default. This is the opposite of the previous implicit caching behavior. You now opt into caching explicitly with "use cache".
The Development Flow
The recommended approach for this project:
- Fetching data — Create queries in
data/queries/, call in Server Components. Use React'scache()for request deduplication. - Mutating data — Create Server Functions in
data/actions/with"use server". Invalidate withrevalidateTag(tag, 'max')+refresh(). - Caching — Add
"use cache"to functions, components, or pages you want to cache across requests.
Basic Usage
Add the directive at the top of a function, then tag the cache for invalidation:
import { cacheTag } from 'next/cache';
import { cache } from 'react';
export const getPublishedPosts = cache(async () => {
'use cache';
cacheTag('posts');
return await prisma.post.findMany({
where: { published: true, archived: false },
orderBy: { createdAt: 'desc' },
});
});The cache() wrapper from React deduplicates calls within a single render. The "use cache" directive caches the result across requests. Together, they eliminate both redundant queries within a request and redundant work across requests.
Cache Invalidation
After mutations, you need to invalidate the cache and update the current user's view:
import { refresh, revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
'use server';
await prisma.post.create({ data });
revalidateTag('posts', 'max'); // Mark stale, revalidate in background
refresh(); // Force immediate re-render
}| Function | Purpose |
|---|---|
revalidateTag(tag, 'max') | Marks cache stale, triggers background revalidation |
updateTag(tag) | Marks cache stale, revalidates immediately (blocking) |
refresh() | Forces immediate client re-render with fresh data |
The 'max' profile serves stale content while revalidating—other users get fast responses while fresh data generates. Use updateTag when you need the mutation to block until fresh data is ready.
Granular Cache Tags
Tag individual items for precise invalidation:
export const getPublishedPostBySlug = cache(async (slug: string) => {
'use cache';
cacheTag(`post-${slug}`);
return await prisma.post.findUnique({
where: { slug, published: true },
});
});When updating a post:
revalidateTag('posts', 'max'); // Invalidate the list
revalidateTag(`post-${slug}`, 'max'); // Invalidate this specific post
refresh();This ensures both the post list and the individual post's detail page update.
Push Dynamic Data Deep
To maximize cacheable content, push dynamic data access as deep as possible in the component tree. Components that read searchParams, cookies(), or headers() become dynamic—wrap them in <Suspense> so static parent content can render immediately.