Complete Guide: Accessing Params in Next.js 15 with TypeScript
Photo by jameswiseman on Unsplash
9 mins read
0 Like
10 Views
Next.js 15 introduced a significant change in how we access route parameters and search params. The most important change is that params must now be awaited before accessing their values. This guide will walk you through everything you need to know about handling params in Next.js 15 with TypeScript.1. Accessing Dynamic Route Params
2. Accessing Catch-All Route Params
What Changed in Next.js 15?
In previous versions of Next.js, params were synchronously available:
TypeScript1// ❌ Old way (Next.js 14 and below)
2export default function Page({ params }: { params: { id: string } }) {
3 const id = params.id; // Direct access
4 return <div>Post ID: {id}</div>;
5}
In Next.js 15, params are now asynchronous and must be awaited:
TypeScript1// ✅ New way (Next.js 15+)
2export default async function Page({ params }: { params: Promise<{ id: string }> }) {
3 const { id } = await params; // Must await
4 return <div>Post ID: {id}</div>;
5}
Understanding Route Segments
Before diving into examples, let's understand the different types of route segments:
- Static segments:
/about,/contact - Dynamic segments:
/posts/[id],/users/[userId] - Catch-all segments:
/blog/[...slug] - Optional catch-all segments:
/shop/[[...categories]]
1. Accessing Dynamic Route Params [id]/page.tsx
File Structure
Plain Text1app/ 2 posts/ 3 [id]/ 4 page.tsx
Server Component Implementation
TypeScript1// app/posts/[id]/page.tsx
2interface PageProps {
3 params: Promise<{
4 id: string;
5 }>;
6}
7
8export default async function PostPage({ params }: PageProps) {
9 // ✅ Await the params
10 const { id } = await params;
11
12 // Now you can use the id
13 console.log('Post ID:', id);
14
15 return (
16 <div>
17 <h1>Post Details</h1>
18 <p>You are viewing post with ID: {id}</p>
19 </div>
20 );
21}
22
23// Optional: Generate metadata using params
24export async function generateMetadata({ params }: PageProps) {
25 const { id } = await params;
26
27 return {
28 title: `Post ${id}`,
29 description: `Details for post ${id}`,
30 };
31}
URLs that match this route:
/posts/123→id = "123"/posts/hello-world→id = "hello-world"/posts/my-first-post→id = "my-first-post"
2. Accessing Catch-All Route Params [...id]/page.tsx
File Structure
Plain Text1app/ 2 blog/ 3 [...slug]/ 4 page.tsx
Server Component Implementation
TypeScript1// app/blog/[...slug]/page.tsx
2interface PageProps {
3 params: Promise<{
4 slug: string[];
5 }>;
6}
7
8export default async function BlogPage({ params }: PageProps) {
9 // ✅ Await the params
10 const { slug } = await params;
11
12 // slug is an array of path segments
13 console.log('Slug array:', slug);
14
15 // Create breadcrumb navigation
16 const breadcrumbs = slug.map((segment, index) => ({
17 name: segment,
18 path: `/blog/${slug.slice(0, index + 1).join('/')}`,
19 }));
20
21 return (
22 <div>
23 <h1>Blog Post</h1>
24 <nav>
25 {breadcrumbs.map((crumb, index) => (
26 <span>
27 <a href="{crumb.path}">{crumb.name}</a>
28 {index < breadcrumbs.length - 1 && ' / '}
29 </span>
30 ))}
31 </nav>
32 <p>Full path: {slug.join('/')}</p>
33 </div>
34 );
35}
URLs that match this route:
/blog/javascript→slug = ["javascript"]/blog/web-development/react→slug = ["web-development", "react"]/blog/2024/january/new-year→slug = ["2024", "january", "new-year"]
3. Multiple Dynamic Segments
You can combine multiple dynamic segments:
File Structure
Plain Text1app/ 2 users/ 3 [userId]/ 4 posts/ 5 [postId]/ 6 page.tsx
Implementation
TypeScript1// app/users/[userId]/posts/[postId]/page.tsx
2interface PageProps {
3 params: Promise<{
4 userId: string;
5 postId: string;
6 }>;
7}
8
9export default async function UserPostPage({ params }: PageProps) {
10 const { userId, postId } = await params;
11
12 return (
13 <div>
14 <h1>User Post</h1>
15 <p>User ID: {userId}</p>
16 <p>Post ID: {postId}</p>
17 </div>
18 );
19}
Matching URLs:
/users/john/posts/my-story→userId = "john",postId = "my-story"
4. Accessing Search Params (Query Parameters)
Search params (query strings) are handled differently from route params.
Server Side - Search Params
TypeScript1// app/search/page.tsx
2interface PageProps {
3 searchParams: Promise<{
4 q?: string;
5 category?: string;
6 page?: string;
7 }>;
8}
9
10export default async function SearchPage({ searchParams }: PageProps) {
11 // ✅ Await the searchParams (Next.js 15+)
12 const { q, category, page } = await searchParams;
13
14 // Convert page to number with fallback
15 const currentPage = page ? parseInt(page, 10) : 1;
16
17 return (
18 <div>
19 <h1>Search Results</h1>
20 {q && <p>Searching for: "{q}"</p>}
21 {category && <p>Category: {category}</p>}
22 <p>Page: {currentPage}</p>
23 </div>
24 );
25}
URL Examples:
/search?q=nextjs→q = "nextjs"/search?q=react&category=tutorial&page=2→q = "react",category = "tutorial",page = "2"
5. Client Side - Using useParams and useSearchParams
For client components, Next.js provides hooks to access params and search params.
useParams Hook
TypeScript1'use client';
2
3import { useParams } from 'next/navigation';
4
5export default function ClientComponent() {
6 // ✅ useParams returns the current params
7 const params = useParams<{ id: string }>();
8
9 // No need to await on client side
10 const id = params.id;
11
12 return (
13 <div>
14 <p>Current ID from client: {id}</p>
15 </div>
16 );
17}
useSearchParams Hook
TypeScript1'use client';
2
3import { useSearchParams } from 'next/navigation';
4import { Suspense } from 'react';
5
6function SearchComponent() {
7 const searchParams = useSearchParams();
8
9 const query = searchParams.get('q');
10 const category = searchParams.get('category');
11
12 return (
13 <div>
14 <p>Query: {query}</p>
15 <p>Category: {category}</p>
16 </div>
17 );
18}
19
20// ✅ Wrap components using useSearchParams with Suspense
21export default function ClientSearchPage() {
22 return (
23 Loading...}>
24
25
26 );
27}
Updating Search Params on Client Side
TypeScript1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function FilterComponent() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 const updateFilter = useCallback((key: string, value: string) => {
11 const params = new URLSearchParams(searchParams.toString());
12 params.set(key, value);
13
14 // Update the URL
15 router.push(`?${params.toString()}`);
16 }, [router, searchParams]);
17
18 return (
19 <div>
20 <button> updateFilter('category', 'tech')}>
21 Filter by Tech
22 </button>
23 <button> updateFilter('sort', 'date')}>
24 Sort by Date
25 </button>
26 </div>
27 );
28}
6. TypeScript Best Practices
Define Proper Types
TypeScript1// types/page-props.ts
2export interface DynamicPageProps {
3 params: Promise;
4 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
5}
6
7// Usage
8interface PostPageProps extends DynamicPageProps<{ id: string }> {}
9
10export default async function PostPage({ params, searchParams }: PostPageProps) {
11 const { id } = await params;
12 const search = await searchParams;
13
14 // Fully typed!
15 return <div>Post {id}</div>;
16}
Handle Optional Parameters
TypeScript1interface OptionalParamsPageProps {
2 params: Promise<{
3 id?: string;
4 category?: string;
5 }>;
6}
7
8export default async function OptionalPage({ params }: OptionalParamsPageProps) {
9 const { id, category } = await params;
10
11 // Handle undefined values
12 if (!id) {
13 return <div>No ID provided</div>;
14 }
15
16 return (
17 <div>
18 <p>ID: {id}</p>
19 {category && <p>Category: {category}</p>}
20 </div>
21 );
22}
7. Error Handling and Validation
Validate Params
TypeScript1import { notFound } from 'next/navigation';
2
3interface PageProps {
4 params: Promise<{ id: string }>;
5}
6
7export default async function PostPage({ params }: PageProps) {
8 const { id } = await params;
9
10 // Validate the ID format
11 if (!/^\d+$/.test(id)) {
12 notFound(); // Triggers 404 page
13 }
14
15 const postId = parseInt(id, 10);
16
17 return <div>Valid Post ID: {postId}</div>;
18}
Handle Async Errors
TypeScript1export default async function PostPage({ params }: PageProps) {
2 try {
3 const { id } = await params;
4 // Your component logic
5 return <div>Post {id}</div>;
6 } catch (error) {
7 console.error('Error accessing params:', error);
8 return <div>Error loading page</div>;
9 }
10}
8. Common Patterns and Examples
Loading States with Suspense
TypeScript1import { Suspense } from 'react';
2
3export default async function PostLayout({
4 children,
5 params,
6}: {
7 children: React.ReactNode;
8 params: Promise<{ id: string }>;
9}) {
10 const { id } = await params;
11
12 return (
13 <div>
14 <header>Post {id}</header>
15 Loading content...</div>}>
16 {children}
17
18
19 );
20}
Combining Route and Search Params
TypeScript1interface CombinedPageProps {
2 params: Promise<{ category: string }>;
3 searchParams: Promise<{
4 page?: string;
5 limit?: string;
6 sort?: string;
7 }>;
8}
9
10export default async function CategoryPage({ params, searchParams }: CombinedPageProps) {
11 // Await both params and searchParams
12 const { category } = await params;
13 const { page = '1', limit = '10', sort = 'date' } = await searchParams;
14
15 const pageNumber = parseInt(page, 10);
16 const limitNumber = parseInt(limit, 10);
17
18 return (
19 <div>
20 <h1>Category: {category}</h1>
21 <p>Page: {pageNumber}</p>
22 <p>Limit: {limitNumber}</p>
23 <p>Sort by: {sort}</p>
24 </div>
25 );
26}
Summary
Key takeaways for Next.js 15 params handling:
- Always await params in server components:
const { id } = await params - searchParams are also async and need to be awaited:
await searchParams - Client-side hooks (
useParams,useSearchParams) don't need awaiting - Wrap useSearchParams components with
Suspense - Type your params properly with TypeScript interfaces
- Handle errors and validate param values
- Use notFound() for invalid routes
This new async pattern in Next.js 15 provides better performance and more predictable behavior, especially in edge environments. While it requires updating existing code, the benefits include improved type safety and better error handling.
Share: