How to Bypass Next.js Cache Components for Draft Mode Preview
Dharmendra
8 min read
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.
The Problem — Cached Components Prevent Editors from Previewing Unpublished Content
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 entirelyimport { cacheLife } from 'next/cache'async function getPost(slug:
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.
Why This Happens — Cache Components Are Designed to Be Static
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.
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 Solution — Implement a Conditional Check to Bypass the Cache Layer
The fix is straightforward once you understand the problem: check draftMode().isEnabledbefore entering any cached scope, and conditionally skip the cache when previewing.
Here's the corrected pattern:
// ✅ Fixed: Conditional cache bypass for draft modeimport { 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.getPost(slug, { draft: true }) return post}export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const { isEnabled: isDraftMode } = await draftMode() // Choose the appropriate fetcher based on draft mode const post = isDraftMode ? await getPostDraft(slug) : await getPostCached(slug) return ( <article> <h1>{post.title}</h1> <div>{post.content}</div> {isDraftMode && ( <div className="draft-indicator"> 📝 Viewing Draft </div> )} </article> )}
The key changes:
Check draftMode() at the component level — before calling any cached functions
Create separate functions for cached vs. uncached data fetching
Conditionally route to the appropriate function based on isEnabled
This pattern ensures that:
Production traffic hits the optimized cache path
Editors with draft cookies get live, uncached content
The cache remains valid (no cache pollution from draft content)
Why Not Use '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.
Prevention — Creating a Reusable Data-Fetching Wrapper
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.tsimport { 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<T>, options: FetcherOptions = {}) { const { tags = [], revalidate = 'hours' } = options // Cached version for production async function cachedFetch() { 'use cache' cacheLife(revalidate) if (tags.length > 0) { for (const tag of tags) { cacheTag(tag) } } return fetcher(false) } // Memoized but uncached version for drafts const draftFetch = cache(() => fetcher(true)) // The wrapper that chooses the right path return async function (): Promise<T> { const { isEnabled } = await draftMode() return isEnabled ? draftFetch() : cachedFetch() }}
Audit your data layer — identify all 'use cache' functions that fetch preview-able content
Implement the wrapper pattern — start with your most-used fetchers
Test the full preview flow — enable draft mode and verify fresh content appears
Set up cache tags — use cacheTag() and revalidateTag() for granular invalidation when content publishes
Monitor for edge cases — check nested layouts and parallel routes for additional cache boundaries
For 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.
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.
Fix: TypeError: Cannot read properties of null (reading 'auth') on Next.js \_not-found
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.
Solved: Next.js ESLint Unknown Options 'useEslintrc' and 'extensions' Error
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.