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 earlierinterface BlogPostPageProps {params: {slug: string}}export default function BlogPostPage({ params }: BlogPostPageProps) {// Access slug directly from paramsconst { slug } = paramsconst post = getPostBySlug(slug)// Render the post...return (<div><h1>{post.title}</h1><div dangerouslySetInnerHTML={{ __html: post.content }} /></div>)}// Generate static paths at build timeexport function generateStaticParams() {const posts = getAllPosts()return posts.map(post => ({slug: post.slug,}))}// Generate metadataexport 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 Promisetype ParamsType = Promise<{ slug: string }>export default async function BlogPostPage({ params }: { params: ParamsType }) {// Must await params before accessing slugconst { slug } = await paramsconst 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 paramsexport async function generateMetadata({ params }: { params: ParamsType }) {const { slug } = await paramsconst 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:
// OLDinterface BlogPostPageProps {params: { slug: string }}// NEWtype 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:
// OLDexport default function BlogPostPage({ params }: BlogPostPageProps) {// ...}// NEWexport 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:
// OLDconst { slug } = paramsconst post = getPostBySlug(params.slug)// NEWconst { slug } = await paramsconst 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:
// OLDexport function generateMetadata({ params }: BlogPostPageProps) {const post = getPostBySlug(params.slug)// ...}// NEWexport async function generateMetadata({ params }: { params: ParamsType }) {const { slug } = await paramsconst 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 Promisetype ParamsType = Promise<{ slug: string }>// Generate the static paths at build timeexport function generateStaticParams() {const posts = getAllPosts()return posts.map(post => ({slug: post.slug,}))}// Generate metadata for the pageexport async function generateMetadata({params,}: {params: ParamsType}): Promise<Metadata> {const { slug } = await paramsconst post = getPostBySlug(slug)if (!post) {return {title: 'Post Not Found',}}return {title: post.title,description: `Read our blog post: ${post.title}`,}}// The page componentexport default async function BlogPostPage({ params }: { params: ParamsType }) {// Must await params in Next.js 15const { slug } = await paramsconst post = getPostBySlug(slug)// Pass the post to a client component for renderingreturn <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 Promiseconst 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.