
Content creator and developer at UICraft Marketplace, sharing insights and tutorials on modern web development.
Save hours of development time with our premium Next.js templates. Built with Next.js 16, React 19, and Tailwind CSS 4.
Get the latest articles, tutorials, and product updates delivered to your inbox.
High-traffic Next.js applications often suffer from poor CDN cache hit rates due to how React Server Component payloads are hashed during client-side navigation. This guide uses real performance benchmarks to demonstrate fixes that dramatically improve edge caching efficiency.

When your Next.js build fails with "TypeError: Cannot read properties of null (reading 'auth')" on the \_not-found page, it's usually because your layout tries to access authentication context during static prerendering. Here's how to fix it properly.

Running `next lint` with ESLint 9 throws cryptic "Unknown options" errors for useEslintrc and extensions. This guide explains why ESLint 9's flat config breaks Next.js linting and provides clear solutions to get your project working again.
If you've recently enabled Cache Components in Next.js 16+ and suddenly noticed your headless CMS preview isn't showing draft content, you're not alone. This is a fundamental conflict between Next.js's aggressive caching strategy and the dynamic nature of content previews.
The traditional draftMode() workflow—which content editors rely on daily—breaks silently when Cache Components enters the picture. Your pages render beautifully fast, but they're serving stale, published content even when draft mode is explicitly enabled.
This guide walks through exactly why Next.js Cache Components draftMode integration requires special handling, and how to implement a robust solution that gives you the best of both worlds: blazing-fast static pages for production users, and live preview for your content team.
Here's a scenario you might recognize: your marketing team is working on a new product launch. They've crafted the perfect blog post in your headless CMS (Sanity, Contentful, Strapi—pick your favorite). They click the "Preview" button expecting to see their draft content, but the page shows the old, published version instead.
The preview URL includes the correct draft token. The __prerender_bypass cookie is set. Yet the content stubbornly refuses to update.
What you're seeing:
// ❌ Broken: This component is cached and ignores draftMode entirely
import { cacheLife } from 'next/cache'
async function getPost(slug: string) {
The 'use cache' directive tells Next.js to aggressively cache this component's output. When editors request the preview URL, Next.js serves the cached version—completely bypassing the draft content fetch.
This isn't a bug. It's working exactly as designed. The problem is that Cache Components don't automatically respect draftMode() cookies.
To understand the fix, you need to understand what Cache Components actually does under the hood.
When you enable Cache Components (cacheComponents: true in next.config.js), Next.js fundamentally changes how it renders your routes. It prerenders a static HTML shell at build time, with placeholders for dynamic content wrapped in <Suspense> boundaries.
From the official Next.js documentation:
Cache Components eliminates these tradeoffs by prerendering routes into a static HTML shell that's immediately sent to the browser, with dynamic content updating the UI as it becomes ready.
The key insight: components marked with 'use cache' are rendered during build time (or cached on first request), not on every request.
The draftMode() function relies on reading a cookie from the incoming request:
import { draftMode } from 'next/headers'
export default async function Page() {
const { isEnabled } = await draftMode()
// isEnabled = true when preview cookie is present
}But here's the conflict: when your data-fetching function is wrapped with 'use cache', it executes before the request context is established. The cache layer has no concept of cookies, headers, or any request-specific data.
This behavior is explicitly documented as a core design principle. As noted in GitHub issue #86739, accessing runtime data like cookies(), headers(), params, or searchParams within cached scopes triggers warnings because these values simply aren't available in that context.
The cache layer is intentionally request-agnostic. That's what makes it fast. But it also means Next.js Cache Components draftMode don't work together out of the box.
The fix is straightforward once you understand the problem: check draftMode().isEnabled before entering any cached scope, and conditionally skip the cache when previewing.
Here's the corrected pattern:
// ✅ Fixed: Conditional cache bypass for draft mode
import { draftMode } from 'next/headers'
import { cacheLife } from 'next/cache'
async function getPostCached(slug: string) {
'use cache'
cacheLife('hours')
const post = await cms.getPost(slug, { draft: false })
return post
}
async function getPostDraft(slug: string) {
// No cache directive - fetches fresh on every request
const post = await cms.
The key changes:
draftMode() at the component level — before calling any cached functionsisEnabledThis pattern ensures that:
'use cache: private'?You might wonder if 'use cache: private' is the answer here. It's not—at least not for this use case.
The 'use cache: private' directive does allow access to runtime data like cookies() and headers(), but the results are cached only in the browser's memory and don't persist across reloads. This creates poor UX for editors who need consistent preview behavior.
More importantly, 'use cache: private' is still experimental and depends on runtime prefetching features that aren't stable yet. The conditional bypass pattern shown above is production-ready and works reliably across all Next.js 16+ versions.
Sprinkling conditional logic throughout your codebase gets messy fast. Instead, create a centralized wrapper that handles the cache/draft logic in one place.
Here's a production-ready pattern:
// lib/data-fetcher.ts
import { draftMode } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import { cache } from 'react'
type FetcherOptions = {
tags?: string[]
revalidate?: 'hours' | 'days' | 'weeks'
}
/**
* Creates a draft-aware cached fetcher
* - Returns cached data for production traffic
* - Returns fresh data when draft mode is enabled
*/
export function createCachedFetcher<T>(
fetcher: (draft: boolean) => Promise
Now your page components stay clean:
// app/blog/[slug]/page.tsx
import { createCachedFetcher } from '@/lib/data-fetcher'
import { cms } from '@/lib/cms'
const getPost = createCachedFetcher(
(draft) => cms.getPost(slug, { draft }),
{ tags: ['posts'], revalidate: 'hours' }
)
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
If your fetcher needs parameters (like a slug), modify the wrapper slightly:
// ✅ Parameterized draft-aware fetcher
export function createCachedFetcherWithParams<P, T>(
fetcher: (params: P, draft: boolean) => Promise<T>,
options: FetcherOptions = {}
) {
const { tags = [], revalidate = 'hours' } = options
async function cachedFetch(params: P) {
'use cache'
cacheLife(revalidate)
tags.forEach(tag => cacheTag(tag))
return
This pattern scales to any number of CMS queries while keeping the Next.js Cache Components draftMode logic consolidated.
Content editors often lose track of whether they're in preview mode. Add a persistent indicator:
// components/DraftModeIndicator.tsx
import { draftMode } from 'next/headers'
export default async function DraftModeIndicator() {
const { isEnabled } = await draftMode()
if (!isEnabled) return null
return (
<div className="fixed bottom-4 right-4 bg-amber-500 text-black px-4 py-2 rounded-full shadow-lg z-50 font-medium">
📝 Draft Mode Active
</div>
)
}Include this in your root layout and editors will always know their preview state.
draftMode() cookies automatically — the 'use cache' directive executes before request context is availabledraftMode().isEnabled before entering cached scopes — route to uncached functions when previewing'use cache: private' for preview — it's experimental and doesn't persist across reloads'use cache' functions that fetch preview-able contentcacheTag() and revalidateTag() for granular invalidation when content publishesFor the full discussion on Cache Components and runtime data access patterns, see GitHub issue #86739—it includes additional context on blocking patterns and workarounds from the Next.js team.