<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ppr</title>
<link>https://coderlegion.com/tag/ppr</link>
<description>Powered by Question2Answer</description>
<item>
<title>Next.js Cache Components Migration Guide (2026)</title>
<link>https://coderlegion.com/26051/next-js-cache-components-migration-guide-2026</link>
<description>&lt;p&gt;You flip on &lt;code&gt;cacheComponents: true&lt;/code&gt; expecting a quick performance win, and instead your build explodes with red errors about &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, and &lt;code&gt;fetchCache&lt;/code&gt; not being allowed anymore. Then, once those clear, the dev overlay starts flagging routes that &quot;won't render instantly&quot; — pointing at &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, and database calls you've had in production for months.&lt;/p&gt;
&lt;p&gt;Nothing is actually broken. This is exactly how the migration is supposed to work — Next.js is walking you through moving from &lt;em&gt;implicit&lt;/em&gt; caching (where the framework guessed what should be static) to &lt;em&gt;explicit&lt;/em&gt; caching (where you say so directly). This guide walks through that migration end to end: every route segment config you'll need to replace, what to swap it for, and how to do it without rewriting your whole app in one sitting.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;New to Cache Components as a concept?&lt;/strong&gt; This guide is about &lt;em&gt;migrating&lt;/em&gt; an existing app. If you want the &quot;what and why&quot; first, read &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Next.js Rendering Strategies Explained&lt;/a&gt;, which covers SSR, SSG, ISR, and Cache Components from scratch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-migration-exists&quot;&gt;Why This Migration Exists&lt;/h3&gt;
&lt;p&gt;Before Cache Components, Next.js decided whether a route was static or dynamic &lt;em&gt;for you&lt;/em&gt;, by inspecting what your code touched — a &lt;code&gt;fetch&lt;/code&gt; call, a &lt;code&gt;cookies()&lt;/code&gt; read, a route segment config. That worked, but it was implicit: two developers could read the exact same route and disagree about whether it was static or dynamic, and a small code change could silently flip a page from instant to slow.&lt;/p&gt;
&lt;p&gt;Cache Components flips the default: &lt;strong&gt;everything is dynamic (rendered per-request) unless you explicitly mark it as cached.&lt;/strong&gt; You use &lt;code&gt;'use cache'&lt;/code&gt; to say &quot;cache this,&quot; &lt;code&gt;cacheLife()&lt;/code&gt; to say &quot;for how long,&quot; and &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; to say &quot;this part is allowed to be slow, stream it in separately.&quot; Nothing is guessed anymore.&lt;/p&gt;
&lt;p&gt;This is also what powers &lt;strong&gt;Partial Prerendering (PPR)&lt;/strong&gt; as the default behavior — a page can ship a static, instantly-served HTML shell while a personalized or slow-loading section streams in right behind it, all in one route, with no extra config.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;This guide assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; — Cache Components requires it. If you're on Next.js 15 with &lt;code&gt;experimental.dynamicIO&lt;/code&gt; or &lt;code&gt;experimental.ppr&lt;/code&gt;, upgrade to 16 first using the &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/upgrading/version-16&quot;&gt;official upgrade guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App Router&lt;/strong&gt; — Cache Components is an App Router feature only. It does nothing for a &lt;code&gt;pages/&lt;/code&gt; directory. A hybrid app (both &lt;code&gt;pages/&lt;/code&gt; and &lt;code&gt;app/&lt;/code&gt;) is fine — the flag only affects &lt;code&gt;app/&lt;/code&gt; routes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode (the examples below use it, but everything works the same in plain JS).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js runtime&lt;/strong&gt; — Cache Components does not support &lt;code&gt;runtime = 'edge'&lt;/code&gt;. More on this later.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-enable-the-cachecomponents-flag&quot;&gt;Step 1: Enable the &lt;code&gt;cacheComponents&lt;/code&gt; Flag&lt;/h3&gt;
&lt;p&gt;Turn it on in your config file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
import type { NextConfig } from &quot;next&quot;;

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; this single flag replaces three older, separate flags — &lt;code&gt;experimental.dynamicIO&lt;/code&gt;, &lt;code&gt;experimental.useCache&lt;/code&gt;, and &lt;code&gt;experimental.ppr&lt;/code&gt;. If your &lt;code&gt;next.config.ts&lt;/code&gt; still has any of those, delete them; &lt;code&gt;cacheComponents&lt;/code&gt; supersedes all three.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Leaving &lt;code&gt;experimental.ppr&lt;/code&gt; or &lt;code&gt;experimental_ppr&lt;/code&gt; route segment exports in place after adding &lt;code&gt;cacheComponents&lt;/code&gt;. Next.js 16 removed the &lt;code&gt;experimental.ppr&lt;/code&gt; flag entirely — PPR is just what &lt;code&gt;cacheComponents&lt;/code&gt; does now. Run &lt;code&gt;npx @next/codemod@canary&lt;/code&gt; to clean up leftover &lt;code&gt;experimental_ppr&lt;/code&gt; segment exports automatically (see the codemod list in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/upgrading/codemods&quot;&gt;official codemods reference&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The moment you save this file, any route that still exports &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, or &lt;code&gt;fetchCache&lt;/code&gt; will fail to build. That's expected — those three route segment configs no longer exist under Cache Components, and the rest of this guide is about replacing each one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-pick-your-migration-strategy&quot;&gt;Step 2: Pick Your Migration Strategy&lt;/h3&gt;
&lt;p&gt;You have two realistic paths, and the right one depends on how big your app is.&lt;/p&gt;
&lt;h4 id=&quot;option-a-migrate-everything-now&quot;&gt;Option A — Migrate everything now&lt;/h4&gt;
&lt;p&gt;Fine for small apps or a handful of routes. Go through each error the build throws, fix it, move to the next.&lt;/p&gt;
&lt;h4 id=&quot;option-b-migrate-incrementally-recommended-for-real-apps-&quot;&gt;Option B — Migrate incrementally (recommended for real apps)&lt;/h4&gt;
&lt;p&gt;Use the &lt;code&gt;instant&lt;/code&gt; route segment config to defer validation on routes you're not ready to touch yet, so the rest of your app keeps shipping while you convert routes one at a time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/layout.tsx
export const instant = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;instant = false&lt;/code&gt; tells Next.js &quot;this segment is allowed to block on request-time work — don't fail the build over it yet.&quot; It does &lt;strong&gt;not&lt;/strong&gt; force the route to become fully dynamic; a route that's actually prerenderable still ships a static shell even with this set. It just silences the validation error so you can come back to it later.&lt;/p&gt;
&lt;p&gt;Next.js ships a codemod that adds this opt-out to every &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;layout&lt;/code&gt;, and &lt;code&gt;default&lt;/code&gt; file in one pass:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @next/codemod@canary cache-components-instant-false ./app
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If your app lives under &lt;code&gt;src/app&lt;/code&gt;, pass that exact path instead — &lt;code&gt;npx @next/codemod@canary cache-components-instant-false ./src/app&lt;/code&gt;. A wrong path silently reports &lt;code&gt;0 ok&lt;/code&gt; instead of erroring, so check the file count in the output before assuming it worked.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With that run, your whole app builds and serves again. Then work through routes one at a time: remove &lt;code&gt;instant = false&lt;/code&gt; from a segment, fix whatever the dev overlay flags, repeat.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;instant = false&lt;/code&gt; doesn't fix everything.&lt;/strong&gt; Synchronous, non-deterministic calls — &lt;code&gt;new Date()&lt;/code&gt;, &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;crypto.randomUUID()&lt;/code&gt; — used during prerender still throw a hard build error that this opt-out does &lt;strong&gt;not&lt;/strong&gt; suppress. Move the call into a component wrapped in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; (calling &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/connection&quot;&gt;&lt;code&gt;connection()&lt;/code&gt;&lt;/a&gt; first) or into a Client Component instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-remove-dynamic-force-dynamic-&quot;&gt;Step 3: Remove &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This one's the easiest fix in the whole migration — you just delete it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const dynamic = &quot;force-dynamic&quot;;

export default function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — every route is already dynamic by default
export default function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; under Cache Components, nothing is cached unless you say so with &lt;code&gt;'use cache'&lt;/code&gt;. &quot;Dynamic by default&quot; is now the baseline behavior, so &lt;code&gt;force-dynamic&lt;/code&gt; has nothing left to force.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-replace-dynamic-force-static-with-use-cache-&quot;&gt;Step 4: Replace &lt;code&gt;dynamic = 'force-static'&lt;/code&gt; with &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Start by deleting the export, then let the build tell you what's actually uncached.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const dynamic = &quot;force-static&quot;;

export default async function Page() {
  const data = await fetch(&quot;https://api.example.com/data&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife } from &quot;next/cache&quot;;

export default async function Page() {
  &quot;use cache&quot;;
  cacheLife(&quot;max&quot;);

  const data = await fetch(&quot;https://api.example.com/data&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;use cache&lt;/code&gt; at the top of the function marks it as a cache boundary — everything inside, including the &lt;code&gt;fetch&lt;/code&gt; call, gets cached automatically. &lt;code&gt;cacheLife('max')&lt;/code&gt; is the closest built-in profile when you want long-lived caching, but it is not a literal equivalent of the old &lt;code&gt;force-static&lt;/code&gt; behavior.&lt;/p&gt;
&lt;p&gt;If your &lt;code&gt;force-static&lt;/code&gt; route reads runtime data (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;), you can't keep it fully static anymore — that data genuinely only exists per-request. The fix there is wrapping just that part in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, covered in &lt;a rel=&quot;nofollow&quot; href=&quot;#update-your-invalidation-calls&quot;&gt;Step 9&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-replace-revalidate-with-cachelife&quot;&gt;Step 5: Replace &lt;code&gt;revalidate&lt;/code&gt; with &lt;code&gt;cacheLife&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A numeric &lt;code&gt;revalidate&lt;/code&gt; export becomes a &lt;code&gt;cacheLife()&lt;/code&gt; call inside a &lt;code&gt;'use cache'&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const revalidate = 3600; // 1 hour

export default async function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife } from &quot;next/cache&quot;;

export default async function Page() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);

  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;cacheLife&lt;/code&gt; accepts either a named profile string or a custom object. The built-in profiles — &lt;code&gt;seconds&lt;/code&gt;, &lt;code&gt;minutes&lt;/code&gt;, &lt;code&gt;hours&lt;/code&gt;, &lt;code&gt;days&lt;/code&gt;, &lt;code&gt;weeks&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt; — map to sensible defaults:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Profile &lt;/th&gt;&lt;th&gt;&lt;code&gt;stale&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;revalidate&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;expire&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;seconds&lt;/code&gt; &lt;/td&gt;&lt;td&gt;30s &lt;/td&gt;&lt;td&gt;1s &lt;/td&gt;&lt;td&gt;1 min&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;minutes&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 min &lt;/td&gt;&lt;td&gt;1 hour&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hours&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 hour &lt;/td&gt;&lt;td&gt;1 day&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;days&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 day &lt;/td&gt;&lt;td&gt;1 week&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weeks&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 week &lt;/td&gt;&lt;td&gt;30 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;30 days &lt;/td&gt;&lt;td&gt;1 year&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If none of the presets match your old &lt;code&gt;revalidate&lt;/code&gt; value exactly, pick the closest one, or define a custom profile in &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
import type { NextConfig } from &quot;next&quot;;

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    blog: {
      stale: 3600, // 1 hour — how long the client can use a cached value without checking
      revalidate: 900, // 15 minutes — how often the server refreshes in the background
      expire: 86400, // 1 day — max age before the entry is dropped entirely
    },
  },
};

export default nextConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;stale&lt;/code&gt; controls how long the client Router Cache can reuse the result; it does not set the HTTP &lt;code&gt;Cache-Control: max-age&lt;/code&gt; header.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/page.tsx
import { cacheLife } from &quot;next/cache&quot;;

export default async function BlogPage() {
  &quot;use cache&quot;;
  cacheLife(&quot;blog&quot;); // references the custom profile above

  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; You can also redefine a built-in profile (including &lt;code&gt;default&lt;/code&gt;) with the same name, if you want &lt;code&gt;cacheLife('hours')&lt;/code&gt; to mean something different across your whole app. Document it somewhere obvious if you do — a teammate reading &lt;code&gt;cacheLife('hours')&lt;/code&gt; will otherwise assume the stock 1-hour behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-delete-fetchcache&quot;&gt;Step 6: Delete &lt;code&gt;fetchCache&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This route segment config has no replacement because it has no purpose anymore.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const fetchCache = &quot;force-cache&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — just delete it
export default async function Page() {
  &quot;use cache&quot;;
  // every fetch() inside this scope is cached automatically
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code&gt;fetchCache&lt;/code&gt; used to control whether &lt;code&gt;fetch()&lt;/code&gt; calls were cached at the route level. Under Cache Components, caching is decided per-function by &lt;code&gt;'use cache'&lt;/code&gt;, not globally per route — so there's nothing left for &lt;code&gt;fetchCache&lt;/code&gt; to configure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-move-fetch-cache-options-into-use-cache-&quot;&gt;Step 7: Move &lt;code&gt;fetch()&lt;/code&gt; Cache Options into &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Move the caching policy into the &lt;code&gt;use cache&lt;/code&gt; scope and replace fetch-level tags with &lt;code&gt;cacheTag()&lt;/code&gt; where appropriate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export default async function Page() {
  const res = await fetch(&quot;https://api.example.com/data&quot;, {
    cache: &quot;force-cache&quot;,
    next: { revalidate: 3600, tags: [&quot;data&quot;] },
  });
  const data = await res.json();
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife, cacheTag } from &quot;next/cache&quot;;

async function getData() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);
  cacheTag(&quot;data&quot;);

  const res = await fetch(&quot;https://api.example.com/data&quot;);
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; pulling the fetch into its own &lt;code&gt;getData()&lt;/code&gt; function marked &lt;code&gt;'use cache'&lt;/code&gt; is the standard pattern — it keeps the cache boundary small and lets the page component stay a plain async function. &lt;code&gt;cacheTag('data')&lt;/code&gt; is what lets you invalidate this specific cached result later (&lt;a rel=&quot;nofollow&quot; href=&quot;#update-your-invalidation-calls&quot;&gt;Step 9&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Persistence difference worth knowing:&lt;/strong&gt; the old &lt;code&gt;fetch&lt;/code&gt; Data Cache persisted across deployments and serverless instances. By default, 'use cache' uses an in-memory cache isolated to the Next.js process. Multiple instances do not share that cache, and entries are lost when the process restarts. If you need caching that survives instance teardown, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/directives/use-cache-remote&quot;&gt;&lt;code&gt;'use cache: remote'&lt;/code&gt;&lt;/a&gt; or configure a &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheHandlers&quot;&gt;cache handler&lt;/a&gt;. Either way, expect values to recompute after a fresh deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-8-replace-unstable-cache-with-use-cache-&quot;&gt;Step 8: Replace &lt;code&gt;unstable_cache&lt;/code&gt; with &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you were wrapping database queries in &lt;code&gt;unstable_cache&lt;/code&gt;, the migration is mechanical.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
import { unstable_cache } from &quot;next/cache&quot;;
import { db } from &quot;@/lib/db&quot;;

export const getUser = unstable_cache(
  async (id: string) =&amp;gt; {
    return db.query.users.findFirst({ where: eq(users.id, id) });
  },
  [&quot;user&quot;], // cache key prefix
  { tags: [&quot;users&quot;], revalidate: 3600 }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife, cacheTag } from &quot;next/cache&quot;;
import { db } from &quot;@/lib/db&quot;;

export async function getUser(id: string) {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);
  cacheTag(&quot;users&quot;);

  return db.query.users.findFirst({ where: eq(users.id, id) });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the wrapper function becomes a plain function with a directive. Notice the manual &lt;code&gt;[&quot;user&quot;]&lt;/code&gt; cache-key-prefix array is gone entirely — Next.js derives the cache key automatically from the function's arguments (and its closure), so you never write a cache key by hand.&lt;/p&gt;
&lt;p&gt;Same persistence caveat as Step 7 applies: &lt;code&gt;unstable_cache&lt;/code&gt; persisted across deployments; &lt;code&gt;'use cache'&lt;/code&gt; doesn't by default.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;update-your-invalidation-calls&quot;&gt;Step 9: Update Your Invalidation Calls&lt;/h2&gt;
&lt;p&gt;On-demand invalidation still works — you just have three tools now instead of one, and picking the right one matters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/updateTag&quot;&gt;&lt;code&gt;updateTag&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — for a mutation where the user needs to see their own change &lt;em&gt;immediately&lt;/em&gt; (read-your-own-writes). Only callable from a Server Action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidateTag&quot;&gt;&lt;code&gt;revalidateTag&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — for stale-while-revalidate behavior. Works in both Server Actions and Route Handlers, but now &lt;strong&gt;requires a cache profile as a second argument&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidatePath&quot;&gt;&lt;code&gt;revalidatePath&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — unchanged from before.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/actions.ts
&quot;use server&quot;;

import { updateTag } from &quot;next/cache&quot;;

export async function createPost(formData: FormData) {
  // create the post in your database, then:
  updateTag(&quot;posts&quot;); // the next render sees the new post immediately
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/webhook/route.ts

// Before
import { revalidateTag } from &quot;next/cache&quot;;

export async function POST() {
  revalidateTag(&quot;posts&quot;);
  return Response.json({ ok: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/webhook/route.ts

// After — a profile is now required
import { revalidateTag } from &quot;next/cache&quot;;

export async function POST() {
  revalidateTag(&quot;posts&quot;, &quot;max&quot;);
  return Response.json({ ok: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the bare, single-argument &lt;code&gt;revalidateTag(tag)&lt;/code&gt; call is deprecated — it still technically compiles if you suppress the TypeScript error, but it may stop working in a future release. Pass &lt;code&gt;max&lt;/code&gt; as the recommended default — it uses the profile's one-year &lt;code&gt;expire&lt;/code&gt; value as the stale-serving window, so requests almost never block waiting on a fresh fetch after invalidation.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Reaching for &lt;code&gt;revalidateTag&lt;/code&gt; after a mutation when you actually want &lt;code&gt;updateTag&lt;/code&gt;. &lt;code&gt;revalidateTag&lt;/code&gt; marks data stale and serves the &lt;em&gt;old&lt;/em&gt; value while it refreshes in the background — a user who just submitted a form could still see their old data on the very next page load. If the user needs to see their own change right away, use &lt;code&gt;updateTag&lt;/code&gt; inside the Server Action instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-10-delete-unstable-nostore&quot;&gt;Step 10: Delete &lt;code&gt;unstable_noStore&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Remove &lt;code&gt;unstable_noStore()&lt;/code&gt; in most migrations. If the operation must intentionally happen at request time and doesn't naturally depend on &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or another request API, use &lt;code&gt;connection()&lt;/code&gt; to opt that work out of prerendering.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
import { unstable_noStore as noStore } from &quot;next/cache&quot;;

export default async function Page() {
  noStore();
  const data = await db.query(&quot;...&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
export default async function Page() {
  const data = await db.query(&quot;...&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a component genuinely needs to run at request time (not just &quot;not be cached&quot;), call &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/connection&quot;&gt;&lt;code&gt;connection()&lt;/code&gt;&lt;/a&gt; before the work and wrap it in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — that's the real replacement when you need to force a dynamic boundary rather than just skip caching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-11-fix-generatestaticparams&quot;&gt;Step 11: Fix &lt;code&gt;generateStaticParams&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Two real changes here for dynamic routes.&lt;/p&gt;
&lt;h4 id=&quot;an-empty-array-is-now-an-error&quot;&gt;An empty array is now an error&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — used to defer every path to runtime, now throws a build error
export async function generateStaticParams() {
  return [];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — return at least one param so Next.js can prerender a shell
export async function generateStaticParams() {
  const posts = await fetch(&quot;https://.../posts&quot;).then((res) =&amp;gt; res.json());
  return posts.slice(0, 1).map((post: { slug: string }) =&amp;gt; ({ slug: post.slug }));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Cache Components needs &lt;code&gt;generateStaticParams&lt;/code&gt; to produce at least one real param so it can prerender a static shell and confirm it's non-empty. Paths you don't return still work fine — Next.js prerenders a generic shell for unknown params and streams the real content at request time.&lt;/p&gt;
&lt;h4 id=&quot;dynamicparams-is-no-longer-supported&quot;&gt;&lt;code&gt;dynamicParams&lt;/code&gt; is no longer supported&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — this now fails the build entirely
export const dynamicParams = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete it. If you were using &lt;code&gt;dynamicParams: false&lt;/code&gt; to reject unknown params, call &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/not-found&quot;&gt;&lt;code&gt;notFound()&lt;/code&gt;&lt;/a&gt; inside the page instead, once you know the param doesn't resolve to real data.&lt;/p&gt;
&lt;h4 id=&quot;await-params-inside-lt-suspense-gt-not-at-the-top&quot;&gt;Await &lt;code&gt;params&lt;/code&gt; inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, not at the top&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — awaiting at the top blocks the whole static shell
export default async function Page({
  params,
}: {
  params: Promise&amp;lt;{ slug: string }&amp;gt;;
}) {
  const { slug } = await params;
  return &amp;lt;Post slug={slug} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { Suspense } from &quot;react&quot;;

export default function Page({ params }: PageProps&amp;lt;&quot;/blog/[slug]&quot;&amp;gt;) {
  return (
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;Post params={params} /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Post({ params }: Pick&amp;lt;PageProps&amp;lt;&quot;/blog/[slug]&quot;&amp;gt;, &quot;params&quot;&amp;gt;) {
  const { slug } = await params;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; passing the &lt;code&gt;params&lt;/code&gt; promise straight through as a prop, then awaiting it inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped child, lets Next.js prerender the shell for unknown/unlisted params instead of blocking the entire page on resolving them first.&lt;/p&gt;
&lt;p&gt;The same pattern applies to &lt;code&gt;usePathname&lt;/code&gt;, &lt;code&gt;useParams&lt;/code&gt;, &lt;code&gt;useSelectedLayoutSegment&lt;/code&gt;, &lt;code&gt;useSelectedLayoutSegments&lt;/code&gt;, and &lt;code&gt;useSearchParams&lt;/code&gt; in Client Components — if the value depends on a dynamic param, wrap the smallest component that reads it in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; rather than the whole tree.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-12-wrap-cookies-headers-and-searchparams-in-lt-suspense-gt-&quot;&gt;Step 12: Wrap &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, and &lt;code&gt;searchParams&lt;/code&gt; in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the change you'll hit most often, because it's the most common source of &quot;instant navigation&quot; validation errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — reading cookies at the top makes the whole route dynamic
import { cookies } from &quot;next/headers&quot;;

export default async function Page() {
  const theme = (await cookies()).get(&quot;theme&quot;)?.value;
  return &amp;lt;Dashboard theme={theme} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — the page shell prerenders; only Dashboard streams at request time
import { cookies } from &quot;next/headers&quot;;
import { Suspense } from &quot;react&quot;;

export default function Page() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;Dashboard /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Dashboard() {
  const theme = (await cookies()).get(&quot;theme&quot;)?.value;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; moving the &lt;code&gt;cookies()&lt;/code&gt; read into a separate component, and wrapping just that component in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, lets everything &lt;em&gt;outside&lt;/em&gt; it (the page's static shell) prerender normally. Only the part that genuinely needs per-request data waits until the actual request.&lt;/p&gt;
&lt;p&gt;The same move applies to &lt;code&gt;searchParams&lt;/code&gt;, which your page receives as a promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Suspense } from &quot;react&quot;;

export default function Page({ searchParams }: PageProps&amp;lt;&quot;/&quot;&amp;gt;) {
  return (
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;Results searchParams={searchParams} /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Results({ searchParams }: Pick&amp;lt;PageProps&amp;lt;&quot;/&quot;&amp;gt;, &quot;searchParams&quot;&amp;gt;) {
  const { query } = await searchParams;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If a cookie or header controls an attribute on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; in the root layout (&lt;code&gt;lang&lt;/code&gt;, &lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;data-theme&lt;/code&gt;), there's no child element to wrap in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — reading it there makes the whole subtree request-bound by nature. Use an inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; that sets the attribute client-side before paint instead; see &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/preventing-flash-before-hydration&quot;&gt;Preventing flash before hydration&lt;/a&gt; for the pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-13-update-get-route-handlers&quot;&gt;Step 13: Update &lt;code&gt;GET&lt;/code&gt; Route Handlers&lt;/h3&gt;
&lt;p&gt;Route Handlers follow the same rules as pages now.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts

// Before
export const dynamic = &quot;force-static&quot;;

export async function GET() {
  const products = await db.query(&quot;SELECT * FROM products&quot;);
  return Response.json(products);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts

// After
import { cacheLife } from &quot;next/cache&quot;;

export async function GET() {
  const products = await getProducts();
  return Response.json(products);
}

async function getProducts() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);

  return db.query(&quot;SELECT * FROM products&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the &lt;code&gt;'use cache'&lt;/code&gt; directive can't be applied to the exported &lt;code&gt;GET&lt;/code&gt; function itself — it goes on a helper function the handler calls instead.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Reading uncached or runtime data in a &lt;code&gt;GET&lt;/code&gt; handler bails out of prerendering by &lt;em&gt;throwing&lt;/em&gt;. If you already wrap your handler body in &lt;code&gt;try/catch&lt;/code&gt;, that block will catch this bail-out too — which can add noisy logs during the build. Set &lt;code&gt;experimental.hideLogsAfterAbort: true&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt; if that gets in your way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-14-handle-generatemetadata-and-generateviewport&quot;&gt;Step 14: Handle &lt;code&gt;generateMetadata&lt;/code&gt; and &lt;code&gt;generateViewport&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;These follow the same component rules — cache external data, or explicitly mark the page as needing runtime data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export async function generateMetadata() {
  const { title, description } = await db.query(&quot;site-metadata&quot;);
  return { title, description };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — cache it, same as any other external data fetch
export async function generateMetadata() {
  &quot;use cache&quot;;
  const { title, description } = await db.query(&quot;site-metadata&quot;);
  return { title, description };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your metadata genuinely depends on runtime data (a signed-in user's name, a cookie), you can't wrap &lt;code&gt;generateMetadata&lt;/code&gt; itself in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — instead, add a small dynamic marker component to the page so the static content still prerenders while the metadata resolves separately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Suspense } from &quot;react&quot;;
import { connection } from &quot;next/server&quot;;

export async function generateMetadata() {
  // reads runtime data — intentional
  return { title: &quot;Personalized Title&quot; };
}

async function Connection() {
  await connection();
  return null;
}

export default function Page() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;article&amp;gt;Static content&amp;lt;/article&amp;gt;
      &amp;lt;Suspense&amp;gt;
        &amp;lt;Connection /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the full breakdown of trade-offs here, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/generate-metadata#with-cache-components&quot;&gt;&lt;code&gt;generateMetadata&lt;/code&gt; with Cache Components&lt;/a&gt; in the official reference — this is one of the trickier corners of the migration and worth reading directly if metadata personalization matters for your app. If you haven't set up metadata at all yet, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-metadata&quot;&gt;How to Add Metadata in Next.js 16&lt;/a&gt; covers the basics first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-15-remove-runtime-edge-&quot;&gt;Step 15: Remove &lt;code&gt;runtime = 'edge'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Cache Components requires the Node.js runtime — Edge isn't supported.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — no longer supported under Cache Components
export const runtime = &quot;edge&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete the export (Node.js is the default anyway). If you specifically need Edge behavior for a route — geolocation checks, ultra-low-latency redirects — reach for &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/file-conventions/proxy&quot;&gt;Proxy&lt;/a&gt; (the renamed &lt;code&gt;middleware.ts&lt;/code&gt;) instead of the Edge runtime on the route itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-gotcha-after-the-build-passes-ui-state-no-longer-resets&quot;&gt;A Gotcha After the Build Passes: UI State No Longer Resets&lt;/h3&gt;
&lt;p&gt;Once your routes build cleanly, there's one behavior change worth testing manually, because it won't show up as a build error: Cache Components preserves component state across client-side navigation using React's &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/reference/react/Activity&quot;&gt;&lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt;&lt;/a&gt; component, instead of unmounting the page you navigated away from.&lt;/p&gt;
&lt;p&gt;In practice: a &lt;code&gt;useState&lt;/code&gt; value, an open dropdown, or scroll position no longer resets just because the user clicked to a different page and back. This is usually a win, but it can surface bugs in code that assumed navigating away meant a clean remount — a dropdown that used to close automatically, a dialog whose focus-on-open effect no longer refires because its state was preserved rather than reset.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Assuming a modal or dropdown will close itself on navigation the way it always used to. If a component should reset when its route or user context changes, reset that state explicitly based on the relevant prop/route change instead of relying on unmount behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;migration-checklist&quot;&gt;Migration Checklist&lt;/h3&gt;
&lt;p&gt;Work through this in order for each route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;cacheComponents: true&lt;/code&gt; set in &lt;code&gt;next.config.ts&lt;/code&gt;; old &lt;code&gt;experimental.dynamicIO&lt;/code&gt; / &lt;code&gt;experimental.useCache&lt;/code&gt; / &lt;code&gt;experimental.ppr&lt;/code&gt; removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt; deleted everywhere&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dynamic = 'force-static'&lt;/code&gt; replaced with &lt;code&gt;'use cache'&lt;/code&gt; + &lt;code&gt;cacheLife('max')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;revalidate&lt;/code&gt; exports replaced with &lt;code&gt;cacheLife()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;fetchCache&lt;/code&gt; exports deleted&lt;/li&gt;
&lt;li&gt;[ ] Individual &lt;code&gt;fetch()&lt;/code&gt; cache options (&lt;code&gt;cache&lt;/code&gt;, &lt;code&gt;next.revalidate&lt;/code&gt;, &lt;code&gt;next.tags&lt;/code&gt;) moved into &lt;code&gt;'use cache'&lt;/code&gt; functions with &lt;code&gt;cacheLife&lt;/code&gt; / &lt;code&gt;cacheTag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;unstable_cache&lt;/code&gt; calls converted to &lt;code&gt;'use cache'&lt;/code&gt; functions&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;revalidateTag&lt;/code&gt; calls updated to pass a cache profile (&lt;code&gt;revalidateTag(tag, 'max')&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Mutations that need read-your-own-writes use &lt;code&gt;updateTag&lt;/code&gt; instead of &lt;code&gt;revalidateTag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;unstable_noStore()&lt;/code&gt; calls removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;generateStaticParams&lt;/code&gt; returns at least one param; &lt;code&gt;dynamicParams&lt;/code&gt; export removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; awaited inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped children, not at the top of the page&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;cookies()&lt;/code&gt; / &lt;code&gt;headers()&lt;/code&gt; reads pushed into &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped components&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;GET&lt;/code&gt; Route Handlers use a &lt;code&gt;'use cache'&lt;/code&gt; helper instead of &lt;code&gt;dynamic = 'force-static'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;generateMetadata&lt;/code&gt; / &lt;code&gt;generateViewport&lt;/code&gt; cache external data or use the dynamic-marker pattern for runtime data&lt;/li&gt;
&lt;li&gt;[ ] No routes still export &lt;code&gt;runtime = 'edge'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] UI that relied on unmount-to-reset behavior (dropdowns, dialogs, forms) tested manually after migration&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;do-i-have-to-migrate-my-whole-app-at-once-&quot;&gt;Do I have to migrate my whole app at once?&lt;/h4&gt;
&lt;p&gt;No. Use &lt;code&gt;export const instant = false&lt;/code&gt; on the layouts or pages that aren't ready, run the &lt;code&gt;cache-components-instant-false&lt;/code&gt; codemod to apply it everywhere in one pass, and convert routes one at a time from there. Your app keeps building and serving the whole time.&lt;/p&gt;
&lt;h4 id=&quot;what-happens-to-my-existing-fetch-cache-and-unstable-cache-data-during-migration-&quot;&gt;What happens to my existing &lt;code&gt;fetch&lt;/code&gt; cache and &lt;code&gt;unstable_cache&lt;/code&gt; data during migration?&lt;/h4&gt;
&lt;p&gt;Existing &lt;code&gt;unstable_cache&lt;/code&gt; usage is still supported, but Next.js 16 recommends migrating it to &lt;code&gt;use cache&lt;/code&gt;. The old fetch/Data Cache model and Cache Components can coexist during migration, but route segment configs such as &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, and &lt;code&gt;fetchCache&lt;/code&gt; must be migrated.&lt;/p&gt;
&lt;h4 id=&quot;why-does-my-build-fail-on-new-date-even-with-instant-false-set-&quot;&gt;Why does my build fail on &lt;code&gt;new Date()&lt;/code&gt; even with &lt;code&gt;instant = false&lt;/code&gt; set?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;instant = false&lt;/code&gt; only defers the &lt;em&gt;validation&lt;/em&gt; check for whether a route renders instantly — it doesn't clear synchronous, non-deterministic calls like &lt;code&gt;new Date()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, or &lt;code&gt;crypto.randomUUID()&lt;/code&gt; used during prerendering. Those still throw a hard build error. Move the call into a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped component (calling &lt;code&gt;connection()&lt;/code&gt; first) or into a Client Component.&lt;/p&gt;
&lt;h4 id=&quot;is-use-cache-the-same-as-the-old-fetch-data-cache-&quot;&gt;Is &lt;code&gt;'use cache'&lt;/code&gt; the same as the old &lt;code&gt;fetch&lt;/code&gt; Data Cache?&lt;/h4&gt;
&lt;p&gt;No, and this trips people up. The old &lt;code&gt;fetch&lt;/code&gt; Data Cache persisted across deployments and serverless instances. The default &lt;code&gt;use cache&lt;/code&gt; handler uses an in-memory LRU cache isolated to each Next.js process. It isn't shared across multiple instances and is lost when the process restarts. Use &lt;code&gt;use cache: remote&lt;/code&gt; or a cache handler if you need durability across deployments.&lt;/p&gt;
&lt;h4 id=&quot;my-revalidatetag-tag-call-now-shows-a-typescript-error-what-changed-&quot;&gt;My &lt;code&gt;revalidateTag(tag)&lt;/code&gt; call now shows a TypeScript error — what changed?&lt;/h4&gt;
&lt;p&gt;The single-argument form is deprecated. &lt;code&gt;revalidateTag&lt;/code&gt; now requires a cache profile as a second argument — &lt;code&gt;revalidateTag('posts', 'max')&lt;/code&gt; uses the recommended stale-while-revalidate profile. The &lt;code&gt;max&lt;/code&gt; cache profile has a &lt;code&gt;30-day&lt;/code&gt; server revalidation interval and a &lt;code&gt;1-year&lt;/code&gt; expiration window.&lt;/p&gt;
&lt;h4 id=&quot;does-cache-components-work-with-the-pages-router-&quot;&gt;Does Cache Components work with the Pages Router?&lt;/h4&gt;
&lt;p&gt;No — it's an App Router-only feature. If your project has both &lt;code&gt;pages/&lt;/code&gt; and &lt;code&gt;app/&lt;/code&gt; directories, &lt;code&gt;cacheComponents: true&lt;/code&gt; only affects the &lt;code&gt;app/&lt;/code&gt; routes; &lt;code&gt;pages/&lt;/code&gt; routes are completely unaffected and need no changes.&lt;/p&gt;
&lt;h4 id=&quot;can-i-use-cache-components-on-vercel-s-edge-runtime-&quot;&gt;Can I use Cache Components on Vercel's Edge Runtime?&lt;/h4&gt;
&lt;p&gt;No. Cache Components requires the Node.js runtime. Remove any &lt;code&gt;runtime = 'edge'&lt;/code&gt; export from routes you're migrating — if you specifically need Edge-level behavior, use Proxy (&lt;code&gt;proxy.ts&lt;/code&gt;) instead of the route-level Edge runtime.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;The Cache Components migration looks scary because it surfaces every implicit assumption your app was making about what's static versus dynamic, all at once, as build errors. But each fix is genuinely mechanical: &lt;code&gt;revalidate&lt;/code&gt; becomes &lt;code&gt;cacheLife&lt;/code&gt;, &lt;code&gt;unstable_cache&lt;/code&gt; becomes a &lt;code&gt;'use cache'&lt;/code&gt; function, and any runtime data read (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, a live database query) gets pushed into a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary so it doesn't block the rest of the page.&lt;/p&gt;
&lt;p&gt;Start with &lt;code&gt;instant = false&lt;/code&gt; on everything you're not ready to touch, get the app building again, then convert routes one at a time using the checklist above. You'll come out the other side with a codebase where every cache decision is explicit instead of guessed — and a meaningfully faster app for it.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Next.js Rendering Strategies Explained: SSR, SSG, CSR, ISR &amp;amp; Cache Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-route-handlers&quot;&gt;Route Handlers Explained in Next.js 16 (Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-metadata&quot;&gt;How to Add Metadata in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/error-handling-in-nextjs-16&quot;&gt;Error Handling in Next.js 16: The Complete 2026 Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/server-vs-client-components-nextjs-16&quot;&gt;Server vs Client Components in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;useful-resources&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/migrating-to-cache-components&quot;&gt;Official Guide: Migrating to Cache Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents&quot;&gt;cacheComponents Config Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/cacheLife&quot;&gt;cacheLife Function Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidateTag&quot;&gt;revalidateTag Function Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/instant-navigation&quot;&gt;Instant Navigation Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26051/next-js-cache-components-migration-guide-2026</guid>
<pubDate>Wed, 02 Sep 2026 04:01:53 +0000</pubDate>
</item>
</channel>
</rss>