Skip to content
ExponaBLOG
REACT & NEXT.JS · DAY 039 / 100

generateStaticParams: a practical React & Next.js guide

Pre-render dynamic routes at build time.

Expona Admin6 min read

The problem with dynamic routes at scale

When you have thousands of blog posts, product pages, or documentation articles, every visitor request for a dynamic route triggers a server computation. By the time your Next.js app regenerates that page on demand, the user sees a delay. generateStaticParams: a practical React & Next.js guide solves this by pre-rendering those dynamic routes at build time, eliminating the wait and giving you predictable performance from day one.

In this guide, we'll walk through how to declare which dynamic pages your app needs to build, combine that with revalidation strategies, and handle edge cases when new content appears after your build completes.

Return the params for pages to build

The core responsibility of generateStaticParams is to tell Next.js which parameter combinations to pre-render. You export an async function from your dynamic route file, fetch your data, and return an array of objects where each object represents one set of route parameters.

Here's a concrete example for a blog:

tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function Post({ params }) {
  const post = await getPostBySlug(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

When you run npm run build, Next.js calls generateStaticParams(), receives the array of slugs, and pre-renders every matching page. If you have 500 blog posts, you get 500 static HTML files in your build output. Visitors load those files instantly from a CDN.

For routes with multiple dynamic segments, return objects with all the necessary keys:

tsx
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
  const products = await getAllProducts();
  return products.map(product => ({
    category: product.category,
    id: product.id,
  }));
}

export default async function ProductDetail({ params }) {
  const product = await getProduct(params.category, params.id);
  return <div>{product.name}</div>;
}

Great for blog posts and product pages

The real-world win with generateStaticParams appears when you're running an e-commerce site or content platform. Take a product catalog with 10,000 SKUs. Without static generation, every customer landing on /products/shoes/12345 triggers a database query, computes HTML, and waits. With generateStaticParams, that page is already built and served as a static file in milliseconds.

Similarly, a blog with hundreds of articles benefits enormously. Each post is rendered once during build, not on every request. Your database load drops. Your Time to First Byte becomes near-instant. Your Lighthouse score improves.

Here's a practical example for a documentation site:

tsx
// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
  const allDocs = await fetchAllDocPages();
  return allDocs.map(doc => ({
    slug: doc.path.split('/'),
  }));
}

export default async function DocPage({ params }) {
  const docContent = await getDocContent(params.slug.join('/'));
  return (
    <div>
      <h1>{docContent.title}</h1>
      <div>{docContent.body}</div>
    </div>
  );
}

The catch-all route syntax [...slug] lets you handle nested documentation structures. Your generateStaticParams returns arrays so Next.js can match them correctly.

Combine with revalidate for fresh content

Static generation at build time is blazingly fast, but your content won't update until the next deployment. In most real applications, you want fresh data without rebuilding every time an editor publishes a new post. That's where revalidation comes in.

Incremental Static Regeneration (ISR) lets you specify a revalidate interval. Next.js will rebuild pages in the background after that time has passed, keeping them fresh without a full redeploy:

tsx
// app/blog/[slug]/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function Post({ params }) {
  const post = await getPostBySlug(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

With revalidate = 60, the first visitor after 60 seconds sees the page being regenerated in the background. Subsequent visitors get the fresh version. You balance performance with freshness.

For more granular control, use the revalidateTag() function. When your editor publishes a new post, trigger a revalidation manually:

tsx
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(request) {
  const { slug } = await request.json();
  revalidateTag(`blog-post-${slug}`);
  return new Response('Revalidated', { status: 200 });
}

// In your page:
export const dynamic = 'force-static';
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function Post({ params }) {
  const post = await getPostBySlug(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

This gives you the best of both worlds: static performance by default, with on-demand freshness when you need it.

dynamicParams controls unknown paths

Here's a scenario: you've built and deployed your site with 500 blog posts. Later, an editor publishes post number 501. If a visitor requests that new post's URL, what happens?

By default, dynamicParams = true in Next.js App Router. The server will attempt to render the unknown route on the fly, fetch the data, and serve it. Performance takes a hit, but the new post is accessible immediately.

Set dynamicParams = false if you want strict control. Unknown routes return a 404:

tsx
// app/blog/[slug]/page.tsx
export const dynamicParams = false;

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function Post({ params }) {
  const post = await getPostBySlug(params.slug);
  return <article>{post.title}</article>;
}

With dynamicParams = false, every route outside your generated set returns 404. This is useful for tightly controlled environments, like an internal admin portal or a marketplace with a fixed catalog.

For most public-facing content sites, leaving dynamicParams = true and combining generateStaticParams with revalidate gives you excellent performance while remaining flexible.

Common mistakes

  • Forgetting to await async operations. Your generateStaticParams is async. If you forget to await your database or API call, you'll get an incomplete or empty params array and silently skip building most of your pages.
  • Returning too many params at once. If you have millions of routes, generateStaticParams will attempt to build them all, ballooning your build time. Consider pagination or on-demand generation for truly large datasets.
  • Not testing with next build locally. generateStaticParams only runs during build. Running next dev won't show you build-time errors. Always do a local build to verify your params are correct.
  • Mixing static and dynamic segments incorrectly. If you have a route like [category]/[...slug], your params object must include both keys. Missing one will cause build failures or silent mismatches.
  • Overcomplicating revalidation logic. Start simple: set revalidate to a reasonable number like 3600 (1 hour). Add tag-based revalidation only when you need per-item refresh control.

Key takeaways

  • generateStaticParams tells Next.js which dynamic routes to pre-render at build time, eliminating on-demand rendering latency for your most visited pages.
  • Return an array of parameter objects, one for each route combination you want to generate. This works with single and multiple dynamic segments.
  • Combine generateStaticParams with revalidate to keep pages fresh without a full rebuild. Use time-based ISR for simple cases, tag-based revalidation for fine-grained control.
  • dynamicParams = true allows new routes to be generated on first request; false makes them 404. Choose based on whether your catalog is fixed or growing.
  • Always test with next build locally to catch parameter mismatches and ensure all intended pages are being generated.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles