How to conditionally cache based on authentication?
How do I handle private/authenticated content with "use cache" in Next.js 16? I have pages that should be cached for public repos but NOT cached for private repos (require authentication). How do I conditionally skip caching or make it per-user?
Perfect! Now I have enough information to provide a comprehensive answer. Let me create a clear response.
##How to Handle Private/Authenticated Content with "use cache" in Next.js 16
Based on the Next.js 16 source code and documentation, here's how to handle the caching scenario you described:
###The Core Pattern: Use "use cache: private"
For content that requires authentication and should NOT be cached server-side, use 'use cache: private':
// app/repo/page.tsx
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RepoContent />
</Suspense>
)
}
async function RepoContent() {
'use cache: private'
cacheLife({ stale: 60 }) // 60 seconds minimum for runtime prefetch
// Can access cookies for authentication
const token = (await cookies()).get('auth-token')?.value
// Fetch based on authentication
const repo = await fetch(`/api/repo`, {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
return <RepoDisplay repo={repo} />
}
Key characteristics of 'use cache: private':
| Aspect | Behavior |
|---|---|
| Server-side cache | ❌ None - never stored on server |
| Browser cache | ✅ Cached only in browser memory during session |
| Runtime API access | ✅ Can access cookies(), headers(), searchParams |
| Persists across reloads | ❌ No (browser-only cache) |
| Per-user caching | ✅ Yes - automatically per-user |
| Static shell prerendering | ❌ Excluded (always runs at runtime) |
###Pattern: Public vs Private Content
For repos with mixed public/private access:
// app/repos/[owner]/[name]/page.tsx
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import { Suspense } from 'react'
export default async function RepoPage({
params,
}: {
params: Promise<{ owner: string; name: string }>
}) {
const { owner, name } = await params
return (
<>
{/* Public content - cached on server for all users */}
<PublicRepoInfo owner={owner} name={name} />
{/* Private content - only cached in browser per-user */}
<Suspense fallback={<p>Loading private data...</p>}>
<PrivateRepoData owner={owner} name={name} />
</Suspense>
</>
)
}
// ✅ PUBLIC: Shared across all users
async function PublicRepoInfo({
owner,
name,
}: {
owner: string
name: string
}) {
'use cache' // Regular cache - server-side
cacheLife('hours')
cacheTag(`repo-${owner}-${name}`)
const repo = await db.repos.findPublic(owner, name)
return <div>{repo.description}</div>
}
// 🔒 PRIVATE: Per-user, browser-only
async function PrivateRepoData({
owner,
name,
}: {
owner: string
name: string
}) {
'use cache: private' // Browser-only, per-user cache
cacheLife({ stale: 120 }) // Must be >= 30 seconds
const token = (await cookies()).get('github-token')?.value
const userId = (await cookies()).get('user-id')?.value
if (!token) {
return <p>Please log in</p>
}
// Will execute for each unique user at runtime
const access = await checkAccess(owner, name, userId, token)
return access.isPrivate ? (
<PrivateContent repoData={access.data} />
) : null
}
###Alternative: Conditional Wrapper Pattern
If you want to conditionally choose between 'use cache' and 'use cache: private' based on authentication status:
// app/repo/conditional-page.tsx
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import { Suspense } from 'react'
export default async function ConditionalPage() {
// Check authentication at the page level
const token = (await cookies()).get('auth-token')?.value
const isAuthenticated = !!token
return (
<div>
<h1>Repository Content</h1>
<Suspense fallback={<p>Loading...</p>}>
{isAuthenticated ? (
<PrivateCachedContent token={token} />
) : (
<PublicCachedContent />
)}
</Suspense>
</div>
)
}
// For public visitors
async function PublicCachedContent() {
'use cache'
cacheLife('hours')
const data = await fetch('/api/public-repo')
return <div>{/* render public content */}</div>
}
// For authenticated users
async function PrivateCachedContent({ token }: { token: string }) {
'use cache: private'
cacheLife({ stale: 60 })
// token is passed as argument, part of cache key
const data = await fetch('/api/private-repo', {
headers: { Authorization: `Bearer ${token}` }
})
return <div>{/* render private content */}</div>
}
###Important Notes
-
'use cache: private'cannot be conditional - The directive itself must be static. You must restructure your component tree to have separate components for each caching strategy. -
Minimum
staletime - Private caches require at least 30 seconds forcacheLife.stale(this is a limitation for runtime prefetching). -
Pass authentication data as arguments - For
'use cache'(not private), you cannot readcookies()directly. Instead, extract and pass values:
// ❌ WRONG - for regular 'use cache'
async function CachedFunction() {
'use cache'
const token = (await cookies()).get('token') // ERROR!
}
// ✅ CORRECT
async function CachedFunction({ token }: { token: string }) {
'use cache'
// token is now part of the cache key
}
// Pass the token from the parent
const token = (await cookies()).get('token')?.value
return <CachedFunction token={token} />
'use cache: private'cannot be customized - Unlike'use cache'and'use cache: remote', you cannot configure custom cache handlers for private caches.
###Files to Reference
- Source:
packages/next/src/server/use-cache/use-cache-wrapper.ts- Shows that private caches are never stored in cache handlers (line 1664) - Docs:
docs/01-app/03-api-reference/01-directives/use-cache-private.mdx- Official documentation - Test Example:
test/e2e/app-dir/use-cache-private/- Real-world examples of private cache usage
END OF POST