Company Logo
Illustration for Handling Dynamic Routes in Next.js 15: The Params Promise Pattern

Handling Dynamic Routes in Next.js 15: The Params Promise Pattern

Next.js 15 introduced a significant breaking change that affects all dynamic routes in the App Router. If you've encountered build errors about params types not matching PageProps or needing to await params, this guide is for you.

The Breaking Change in Next.js 15

In Next.js 15, the params object that gets passed to your page components in dynamic routes (like /blog/[slug] or /products/[id]) is now a Promise that needs to be awaited before you can access its properties.

This change is part of optimizations to the App Router in Next.js 15, but it requires code updates to existing applications that use dynamic routes.

Understanding the Error

If you're trying to build a Next.js 15 application with dynamic routes, you might encounter an error like this:

Type error: Type '{ params: { slug: string; }; }' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ slug: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]

Or during runtime:

Error: Route "/blog/[slug]" used params.slug. params should be awaited before using its properties.

This is happening because your code is treating params as a regular object when Next.js 15 now expects it to be handled as a Promise.

The Old Pattern (Next.js 14 and earlier)

Before Next.js 15, dynamic route parameters were accessed directly. Here's how a typical blog post page might have looked:

// Old pattern - Next.js 14 and earlier
interface BlogPostPageProps {
params: {
slug: string
}
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
// Access slug directly from params
const { slug } = params
const post = getPostBySlug(slug)
// Render the post...
return (
<div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
)
}
// Generate static paths at build time
export function generateStaticParams() {
const posts = getAllPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
// Generate metadata
export function generateMetadata({ params }: BlogPostPageProps) {
const post = getPostBySlug(params.slug)
return {
title: post.title,
}
}

The New Pattern (Next.js 15)

In Next.js 15, we need to make several important changes to our code to handle dynamic route parameters correctly:

  • Define params as a Promise type
  • Make the function async
  • await the params before accessing its properties

Here's how to update the previous example to work with the new Promise-based params pattern:

// New pattern - Next.js 15
// Define params as a Promise
type ParamsType = Promise<{ slug: string }>
export default async function BlogPostPage({ params }: { params: ParamsType }) {
// Must await params before accessing slug
const { slug } = await params
const post = getPostBySlug(slug)
// Render the post...
return (
<div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
)
}
// Generate static paths (unchanged functionality, but type should be updated)
export function generateStaticParams() {
const posts = getAllPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
// Generate metadata - also needs to be async and await params
export async function generateMetadata({ params }: { params: ParamsType }) {
const { slug } = await params
const post = getPostBySlug(slug)
return {
title: post.title,
}
}

Detailed Explanation of the Changes

Let's break down the key changes required to update your dynamic routes for Next.js 15:

1. Type Definition

The first change is in how we define the type for the params object. We need to modify our type definitions to reflect that params is now a Promise-based object:

// OLD
interface BlogPostPageProps {
params: { slug: string }
}
// NEW
type ParamsType = Promise<{ slug: string }>

This new type definition accurately reflects that params is now a Promise that will resolve to an object containing the route parameters. The Promise wrapper is crucial for Next.js 15's new handling of dynamic route parameters.

2. Component Signature

Next, we need to update the component signature to handle the asynchronous nature of params. This requires changing both the function declaration and the type annotation:

// OLD
export default function BlogPostPage({ params }: BlogPostPageProps) {
// ...
}
// NEW
export default async function BlogPostPage({ params }: { params: ParamsType }) {
// ...
}

There are two important changes here:

  • The function is now declared as async to allow awaiting the params
  • We're using the Promise-based type for params instead of the old direct object type

3. Accessing Parameters

The most critical change is in how we access the parameters within the component. In Next.js 15, we must use await before attempting to use any properties:

// OLD
const { slug } = params
const post = getPostBySlug(params.slug)
// NEW
const { slug } = await params
const post = getPostBySlug(slug)

In Next.js 15, we must await the params Promise before trying to access any of its properties. This is mandatory - attempting to access properties directly will cause runtime errors.

4. Metadata Generation

Any other functions that use params, like generateMetadata, also need to be updated to handle the Promise-based params object:

// OLD
export function generateMetadata({ params }: BlogPostPageProps) {
const post = getPostBySlug(params.slug)
// ...
}
// NEW
export async function generateMetadata({ params }: { params: ParamsType }) {
const { slug } = await params
const post = getPostBySlug(slug)
// ...
}

The metadata function must also be async and properly await the params object before using its properties.

A Real-World Example

Here's a more complete example showing how to properly handle params in a blog post page using Next.js 15. This demonstrates the full implementation with imports, type definitions, and proper rendering:

import { getPostBySlug, getAllPosts } from '@/lib'
import type { Metadata } from 'next'
import { BlogPostContent } from '@/components'
// Define the params type as a Promise
type ParamsType = Promise<{ slug: string }>
// Generate the static paths at build time
export function generateStaticParams() {
const posts = getAllPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
// Generate metadata for the page
export async function generateMetadata({
params,
}: {
params: ParamsType
}): Promise<Metadata> {
const { slug } = await params
const post = getPostBySlug(slug)
if (!post) {
return {
title: 'Post Not Found',
}
}
return {
title: post.title,
description: `Read our blog post: ${post.title}`,
}
}
// The page component
export default async function BlogPostPage({ params }: { params: ParamsType }) {
// Must await params in Next.js 15
const { slug } = await params
const post = getPostBySlug(slug)
// Pass the post to a client component for rendering
return <BlogPostContent post={post} />
}

This pattern separates the data fetching in the server component from the rendering in a client component, which is a common pattern in Next.js applications. By properly awaiting the params, we ensure our code works correctly with Next.js 15's new approach to dynamic routes.

Using with Client Components

If you're working with client components and need to access the dynamic route parameters, you can use the useParams hook from 'next/navigation'. The behavior in client components is different from server components:

'use client'
import { useParams } from 'next/navigation'
export function MyClientComponent() {
// In client components, useParams returns the params directly, not as a Promise
const params = useParams<{ slug: string }>()
const slug = params.slug
// Rest of your component...
}

An important distinction: in client components, useParams returns the parameters directly as an object, not as a Promise, so you don't need to await them. This is because the Promise handling is only required in server components.

Conclusion

The switch to Promise-based params in Next.js 15 represents a significant change in how dynamic routes work. While it requires updating your code, it's part of the ongoing improvements to the App Router for better performance.

Remember these key points:

  • In server components, params is now a Promise that must be awaited
  • All functions that use params need to be async
  • In client components, use useParams which still returns a regular object

By following these patterns, you'll ensure your Next.js 15 application handles dynamic routes correctly and efficiently.

If you're migrating from Next.js 14 or earlier, you might want to use the codemod provided by Next.js:

npx @next/codemod@latest next-async-request-api .

This will automatically update your codebase to follow the new pattern for handling dynamic route parameters in Next.js 15.