2024-10-30 Web Development

Upgrading to Async Promise-Based searchParams in Next.js 15

By O. Wolfson

Next.js 15 introduces several enhancements to improve server-side rendering, parallel data fetching, and integration with React 19. One key change is that dynamic parameters like searchParams and params are now treated as Promises. This adjustment is part of a broader push in Next.js 15 to optimize server performance by making server-side data fetching fully asynchronous and parallel.

In this guide, we’ll focus on how to handle the new Promise-based searchParams type, with a brief overview of the other major changes in Next.js 15.

Why Async Parameters in Next.js 15?

The shift to Promise-based searchParams and params allows for:

  • Enhanced Streaming SSR: Async parameters help Next.js render content faster by retrieving route parameters in parallel with other data sources.
  • Parallel Data Fetching: Parameters are fetched concurrently with data, minimizing server load time.
  • Better Server Component Support: Integrating with React 19’s server components, which benefits from concurrent and async rendering.

This upgrade improves performance and simplifies handling of dynamic data in Next.js applications.


How searchParams Changes from Next.js 14 to Next.js 15

Previous Approach (Next.js 14)

In Next.js 14, searchParams and params were synchronously available in server components. This made them easy to access directly:

typescript
// Next.js 14: Direct synchronous access to `searchParams`
export default function Page({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  return <div>Query: {searchParams.query}</div>;
}

New Approach (Next.js 15)

With Next.js 15, searchParams is now a Promise, meaning it must be awaited in server components. This allows Next.js to fetch these parameters in parallel with other data, optimizing performance.

typescript
// Next.js 15: `searchParams` as a Promise
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  const resolvedParams = await searchParams;
  return <div>Query: {resolvedParams.query}</div>;
}

Practical Implications

With this change, you’ll need to ensure that all components and functions accessing searchParams are handling it asynchronously, awaiting it where necessary.


Examples of Using Async searchParams

1. Basic Page Component

In a typical page component, you would access searchParams by awaiting it before use. Here’s how it looks:

typescript
// Next.js 15: Await `searchParams` in page components
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  const resolvedParams = await searchParams;
  return <div>Query: {resolvedParams.query}</div>;
}

2. Parallel Data Fetching with searchParams

With async searchParams, you can fetch multiple data sources concurrently, which is especially useful in data-intensive applications:

typescript
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  // Fetch searchParams and API data concurrently
  const [resolvedParams, userData] = await Promise.all([
    searchParams,
    fetch('/api/user').then(res => res.json())
  ]);

  return (
    <div>
      <div>Query: {resolvedParams.query}</div>
      <div>User: {userData.name}</div>
    </div>
  );
}

This approach optimizes performance, reducing server response time by handling all async operations in parallel.


Best Practice: Passing Resolved searchParams to Client Components

When using searchParams with client components, it’s best to resolve it in the server component first. Then, pass the resolved value down as a prop, avoiding unnecessary Promise handling in the client component.

Server Component Example:

typescript
// Server component: Resolve `searchParams` and pass down
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
  const resolvedParams = await searchParams;
  return <ClientComponent params={resolvedParams} />;
}

Client Component Example:

typescript
// Client component: Directly use resolved params
'use client';

export default function ClientComponent({
  params,
}: {
  params: { [key: string]: string | string[] | undefined };
}) {
  return <div>Query: {params.query}</div>;
}

Brief Overview of Other Next.js 15 Changes

Along with the async handling of searchParams, Next.js 15 introduces other notable updates:

  • React 19 Compatibility: Next.js 15 is optimized to work with React 19, which includes new hooks like useActionState and use for handling promises.
  • Async Dynamic APIs: Synchronous APIs like cookies, headers, and draftMode are now async, making data access more flexible and performant.
  • Improved SSR with Streaming: Enhanced streaming support for SSR enables faster page loads, especially for data-heavy applications.

For more details, see the Next.js 15 documentation.