export const dynamic

Forces a route segment to render fully statically or fully per request under the previous caching model.

Signature
export const dynamic = 'auto'
// 'auto' | 'force-dynamic' | 'error' | 'force-static'
  • Removed in PHP v16.0.0 (when cacheComponents is enabled)

Parameters

ParameterTypeDescription
$'auto'optionalstring literal= 'auto'Default. Caches as much as possible without preventing any component from opting into dynamic behavior.
$'force-dynamic'optionalstring literalForces dynamic rendering, so the route renders for each user at request time. Equivalent to setting every fetch() in the layout or page to { cache: 'no-store', next: { revalidate: 0 } } and setting fetchCache = 'force-no-store'.
$'error'optionalstring literalForces prerendering and errors if any component uses a Request-time API or uncached data. Equivalent to getStaticProps() in the pages directory, setting every fetch() to { cache: 'force-cache' } and setting fetchCache = 'only-cache'.
$'force-static'optionalstring literalForces prerendering by making cookies(), headers() and useSearchParams() return empty values. revalidate, revalidatePath and revalidateTag still work in a force-static route.

Errors

With dynamic = 'error', the build fails if any component in the segment reads a Request-time API such as cookies() or headers(), or performs an uncached data fetch.

Changelog

  • PHP v16.0.0dynamic, dynamicParams, revalidate and fetchCache are removed when Cache Components (the cacheComponents flag) is enabled. The option only exists in the previous caching model, documented under Caching and Revalidating (Previous Model).

Next.js decides on its own whether a segment can be prerendered: it prerenders until something forces request-time work. dynamic takes that decision away from the compiler for one layout, page, or route file.

When to reach for it

  • A route that must be fresh but has nothing in it that proves so. A page reading a feature-flag SDK or an in-process clock looks static to the compiler. 'force-dynamic' settles it.
  • A guard rail on a page you never want to become dynamic. 'error' turns an accidental cookies() call in a shared component into a build failure instead of a silent, per-request page.
  • A page whose components read request APIs you do not actually need. 'force-static' prerenders it anyway.

What trips people up

'force-static' does not error on cookies() or headers() — it makes them return empty values. An auth check written as if (!cookieStore.get('session')) redirect('/login') will behave as though nobody is ever signed in, and the wrong page gets baked into the build output.

'force-dynamic' also rewrites every fetch() in the segment to no-store, so a request you deliberately cached with force-cache stops being cached.

The whole option belongs to the pre-16 caching model. Turn on cacheComponents and it is gone; caching becomes per-function with 'use cache'.

Example

Example · typescript
export const dynamic = 'force-dynamic'

type Rate = { pair: string; value: number }

export default async function Page() {
  const res = await fetch('https://api.example.com/rates')
  const rates: Rate[] = await res.json()

  return (
    <ul>
      {rates.map((rate) => (
        <li key={rate.pair}>
          {rate.pair}: {rate.value}
        </li>
      ))}
    </ul>
  )
}

When to use it

  • Force a dashboard that reads a feature-flag service to re-render on every request.
  • Fail the build if a shared component starts calling cookies() inside a marketing page that must stay static.
  • Prerender a page whose UI library calls useSearchParams() but does not need the real query string.

Frequently asked questions

What is the difference between dynamic = 'force-dynamic' and revalidate = 0?

Both make the segment render per request, but they change fetch defaults differently: revalidate = 0 only changes fetches with no cache option to 'no-store' and leaves fetches that opted into 'force-cache' or a positive revalidate alone, while 'force-dynamic' additionally applies fetchCache = 'force-no-store', which overrides even an explicit 'force-cache'.

Why does cookies() return nothing in my page?

Because the segment exports dynamic = 'force-static', which forces cookies(), headers() and useSearchParams() to return empty values so the route can be prerendered. Remove the export, or move the code that needs real request data into a segment that is not force-static.

Does export const dynamic still work in Next.js 16?

Yes, unless you enable the cacheComponents flag — Next.js 16.0 removes dynamic, dynamicParams, revalidate and fetchCache for Cache Components apps. Projects staying on the previous caching model keep using it; see the Caching and Revalidating (Previous Model) guide.

More examples

Guarding a static marketing page with 'error'

The page builds as long as getPlans() reads cached data. The moment somebody adds a component under this route that calls cookies(), headers(), or an uncached fetch, next build fails with an error naming the segment instead of quietly shipping a per-request page.

Example · typescript
// app/(marketing)/pricing/page.tsx
export const dynamic = 'error'

import { getPlans } from '@/lib/plans'

export default async function PricingPage() {
  const plans = await getPlans()

  return (
    <section>
      {plans.map((plan) => (
        <article key={plan.id}>
          <h2>{plan.name}</h2>
          <p>{plan.priceLabel}</p>
        </article>
      ))}
    </section>
  )
}

force-static with an empty cookie store

The page is prerendered once. cookies() resolves to an empty store, so theme is always the 'light' fallback in the generated HTML — which is why force-static must never sit above code that makes authorisation decisions from cookies.

Example · typescript
// app/(public)/help/page.tsx
import { cookies } from 'next/headers'

export const dynamic = 'force-static'

export default async function HelpPage() {
  const cookieStore = await cookies()
  // Under force-static this is always undefined at build time.
  const theme = cookieStore.get('theme')?.value ?? 'light'

  return <main data-theme={theme}>Help centre</main>
}

Route handler that must not be cached

Since Next.js 15 the default caching for GET Route Handlers changed from static to dynamic, so this handler already runs on every request even without the export. What force-dynamic still adds here is the implied fetchCache = 'force-no-store', which turns the upstream fetch into a no-store request even if a call site had asked for 'force-cache'.

Example · typescript
// app/api/health/route.ts
export const dynamic = 'force-dynamic'

export async function GET() {
  const upstream = await fetch('https://api.example.com/health')

  return Response.json({
    ok: upstream.ok,
    status: upstream.status,
  })
}

Discussion

  • Be the first to comment on this lesson.