export const dynamicParams

Controls whether params missing from generateStaticParams render on demand or return a 404.

Signature
export const dynamicParams = true // true | false
  • Removed in PHP v16.0.0 (when cacheComponents is enabled)

Parameters

ParameterTypeDescription
$trueoptionalboolean literal= trueDefault. Dynamic route segments not included in generateStaticParams are generated at request time.
$falseoptionalboolean literalDynamic route segments not included in generateStaticParams return a 404.

Errors

It does not throw: an unlisted param renders the not-found UI with a 404 status when dynamicParams is false.

Changelog

  • PHP v16.0.0dynamic, dynamicParams, revalidate and fetchCache are removed when Cache Components (the cacheComponents flag) is enabled.

dynamicParams answers one question: a visitor asks for /docs/does-not-exist, and generateStaticParams never returned does-not-exist — what happens? With the default true, Next.js renders the segment at request time and caches the result. With false, it returns a 404 without running your page.

When to reach for false

Set it when the set of valid paths is genuinely closed and known at build time — MDX files on disk, a fixed list of locales, a curated set of landing pages. It converts a data-layer 404 into a routing-layer 404, which means your page component never runs for a bad slug and cannot leak a stack trace or an expensive upstream call to a crawler probing random URLs.

What trips people up

The default is a change of behaviour from the pages directory, where you had to opt in to on-demand generation with fallback: true | 'blocking'. Here you are opted in already, so a route backed by a slow CMS will happily render whatever slug is typed at it until you say otherwise.

The other one: leaving dynamicParams = false on a route whose generateStaticParams is filtered in development (a common "only build 10 pages locally" optimisation) makes every other page 404 in production too.

Example

Example · typescript
export const dynamicParams = false

export function generateStaticParams() {
  return [{ slug: 'installation' }, { slug: 'routing' }, { slug: 'caching' }]
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const { default: Content } = await import(`@/content/docs/${slug}.mdx`)

  return <Content />
}

When to use it

  • Restrict a docs route to the slugs that exist as MDX files so unknown paths 404 immediately.
  • Lock a localised route to a fixed list of locales instead of rendering any two-letter code.
  • Keep a crawler from triggering expensive on-demand renders for random product IDs.

Frequently asked questions

How do I make Next.js return a 404 for params that were not prerendered?

Export dynamicParams = false from the page or layout that also defines generateStaticParams — any param outside that list then returns a 404 instead of rendering at request time.

What replaced getStaticPaths fallback in the App Router?

dynamicParams replaces it: dynamicParams = true behaves like fallback: 'blocking' and dynamicParams = false behaves like fallback: false.

Why do new blog posts 404 until I redeploy?

Because dynamicParams = false restricts the route to the params generateStaticParams returned at build time; set it to true (the default) so new slugs render on demand.

More examples

The default: unlisted params render on demand

The top 100 SKUs are prerendered at build time; the rest are rendered on the first request and cached afterwards. Because unlisted params still reach the page, the page itself is responsible for calling notFound() when the SKU does not exist.

Example · typescript
// app/products/[sku]/page.tsx

// dynamicParams defaults to true, so this export is redundant —
// shown here only to make the behaviour explicit.
export const dynamicParams = true

export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/products/top-100')
  const products: { sku: string }[] = await res.json()

  return products.map((product) => ({ sku: product.sku }))
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ sku: string }>
}) {
  const { sku } = await params
  const res = await fetch(`https://api.example.com/products/${sku}`)

  if (!res.ok) {
    // A genuinely missing product still has to 404 itself.
    const { notFound } = await import('next/navigation')
    notFound()
  }

  const product: { name: string } = await res.json()
  return <h1>{product.name}</h1>
}

A closed set of locales

/en, /de and /ja are built; /fr returns a 404 from the router. Setting it on the layout applies the restriction to every page nested under the locale segment, so no child page has to validate the locale itself.

Example · typescript
// app/[lang]/layout.tsx
export const dynamicParams = false

export function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'de' }, { lang: 'ja' }]
}

export default async function LangLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ lang: string }>
}) {
  const { lang } = await params

  return (
    <html lang={lang}>
      <body>{children}</body>
    </html>
  )
}

The trap: a filtered generateStaticParams

Shows why the two settings interact badly: with dynamicParams = false the list returned by generateStaticParams is the complete set of routes that exist, so anything the function filters out becomes a hard 404 rather than a page rendered on demand.

Example · typescript
// app/blog/[slug]/page.tsx
export const dynamicParams = false

export async function generateStaticParams() {
  const posts: { slug: string }[] = await fetch(
    'https://api.example.com/posts'
  ).then((res) => res.json())

  // Speeds up local builds — and, combined with dynamicParams = false,
  // 404s every post past the tenth in any environment where it applies.
  if (process.env.NODE_ENV === 'development') {
    return posts.slice(0, 10).map((post) => ({ slug: post.slug }))
  }

  return posts.map((post) => ({ slug: post.slug }))
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return <article>{slug}</article>
}

Discussion

  • Be the first to comment on this lesson.