This is the new canonical way to think about data + caching in Next.js 16+. It eliminates the "why is this static?" confusion from earlier App Router versions.
Next.js 16+ introduces a fundamentally different approach to caching. Understanding this shift is crucial for working with the new APIs.
Everything is dynamic by default. You explicitly opt into caching with the "use cache" directive. This makes behavior predictable and eliminates confusion about what's static vs dynamic.
The "use cache" directive is the new way to opt into caching for components and functions.
Not signed in
1// Opt into caching with "use cache"2export async function CachedUserCard() {3 "use cache"; // This component is cached45 const user = await currentUser();67 return (8 <div>9 <h3>{user.fullName}</h3>10 <p>{user.email}</p>11 </div>12 );13}
Not signed in
1// No "use cache" = dynamic by default2export async function DynamicUserCard() {3 // No directive = runs at request time45 const user = await currentUser();6 const timestamp = new Date().toISOString();78 return (9 <div>10 <h3>{user.fullName}</h3>11 <p>Rendered: {timestamp}</p>12 </div>13 );14}
cacheLife profiles make SWR (Stale-While-Revalidate) behavior explicit. No more guessing when data will revalidate!
profileMaximum cache duration. Use for data that rarely changes.
Examples: Site configuration, static content, public data
profileCache for hours. Good balance for semi-dynamic data.
Examples: User data, product catalogs, news feeds
profileCache for days. Use for stable data with infrequent updates.
Examples: Archive pages, historical data, documentation
1// Using cacheLife profiles in "use cache"2export async function MyComponent() {3 "use cache": { profile: "hours" } // Explicit cache duration45 const data = await fetchData();6 return <div>{data}</div>;7}89// Custom profiles can be defined in next.config.js10// cacheLife: {11// custom: {12// stale: 3600, // 1 hour13// revalidate: 900, // 15 minutes14// expire: 86400, // 1 day15// }16// }
Next.js 16+ introduces powerful new APIs for cache management with explicit, predictable behavior.
Now requires a cacheLife profile for explicit SWR behavior. No more guessing!
Try different cacheLife profiles to see explicit SWR behavior:
Cache for hours
How it works: revalidateTag now requires a cacheLife profile, making SWR behavior explicit. No more guessing when data will revalidate!
1"use server";23import { revalidateTag } from "next/cache";45export async function updateUserData() {6 // Revalidate with explicit cacheLife profile7 revalidateTag("user-data", "hours");89 // Profile options: 'max', 'hours', 'days', or custom10 revalidateTag("analytics", "max");11 revalidateTag("config", "days");1213 return { success: true };14}
Server Actions only. Provides read-your-writes semantics by expiring cache and immediately re-reading fresh data.
Server Actions only. Provides immediate consistency for user updates:
How it works: updateTag expires cache + re-reads immediately, ensuring you always see your writes. Perfect for profile updates!
1"use server";23// Server Action with updateTag4export async function updateProfile(data) {5 // Update user profile in database6 await db.users.update(userId, data);78 // Use updateTag for read-your-writes semantics9 // This expires cache + re-reads immediately10 updateTag("user-profile");1112 // Subsequent reads get fresh data13 const updated = await fetchUserProfile();14 return updated;15}1617// Perfect for:18// - User profile updates19// - Settings changes20// - Any update where you need immediate consistency
Server Actions only. Refreshes uncached (dynamic) data without touching the cache at all.
Refresh dynamic data without touching the cache:
How it works: refresh() only reloads uncached (dynamic) data. Cached data is unaffected, giving you precise control.
1"use server";23export async function refreshDynamicData() {4 // refresh() only affects uncached data5 // Cached data is completely unaffected6 refresh();78 // This gives you surgical precision:9 // - Reload dynamic server data10 // - Keep cached data intact11 // - No unintended cache invalidation1213 return { success: true };14}1516// Use when:17// - You have mix of cached + uncached data18// - Want to reload only dynamic parts19// - Need to avoid invalidating cache
Different data types need different cache strategies. You can combine multiple tags with appropriate profiles.
Revalidate different types of data with appropriate cache strategies:
user-data
Profile: hours
app-config
Profile: days
analytics
Profile: max
Strategy: Different data types need different cache durations. User data changes frequently (hours), config rarely (days), analytics can be stale (max).
1"use server";23export async function revalidateAppData() {4 // User data: changes frequently5 revalidateTag("user-data", "hours");67 // App config: rarely changes8 revalidateTag("app-config", "days");910 // Analytics: can be stale11 revalidateTag("analytics", "max");1213 // This gives you fine-grained control over14 // cache invalidation strategies per data type15}
Cache Components work seamlessly with PPR, allowing you to mix static shells with dynamic islands without sacrificing fast initial loads.
✓ Pre-rendered at build time or on demand
✓ Served instantly from cache
✓ Great for layout, navigation, static content
⚡ Rendered at request time
⚡ Fresh, personalized data
⚡ Seamlessly integrated with static shell
1// PPR Example: Mix static + dynamic2export default function DashboardPage() {3 return (4 <>5 {/* Static Shell: Pre-rendered, cached */}6 <Header />7 <Navigation />89 {/* Dynamic Island: Fresh user data */}10 <Suspense fallback={<Skeleton />}>11 <DynamicUserWidget />12 </Suspense>1314 {/* Static Shell: Pre-rendered content */}15 <StaticContent />1617 {/* Dynamic Island: Real-time analytics */}18 <Suspense fallback={<Skeleton />}>19 <LiveAnalytics />20 </Suspense>21 </>22 );23}2425// Enable PPR in next.config.js:26// experimental: {27// ppr: true28// }
For context, here's the old way using React's cache() function. This still works but is request-scoped only.
1import { auth } from "@clerk/nextjs/server";2import { cache } from "react";34// Request-scoped caching only5const getCachedAuth = cache(async () => {6 console.log("🔍 getCachedAuth() executed");7 const authData = await auth();8 return authData;9});1011// Call multiple times - only executes once per request12const auth1 = await getCachedAuth();13const auth2 = await getCachedAuth();14const auth3 = await getCachedAuth();
This component calls getCachedAuth() three times. Check your server console - you should only see one log message, proving the function only executed once.
Result: All three calls return the same cached data. The function only executed once (check server console for the 🔍 log).
Calling getCachedUser() multiple times returns the same user object without additional API calls.
No user signed in. Sign in to see cached user data.
getCachedAuth()getCachedUser()getCachedAuth()getUserMetadata()getCachedUser()getUserMetadata()💡 Recommendation: Use "use cache" directive for new code. It's more powerful and flexible.