DEV Community

Omoshola E.
Omoshola E.

Posted on

# πŸš€ Next.js 15: The searchParams Breaking Change You Need to Know About

Understanding the shift from synchronous objects to Promises and how to migrate your code


Released on October 21st, 2024, Next.js 15 introduced several exciting features, but one change caught many developers off guard: searchParams is now a Promise. If you've been working with Next.js 14 and are planning to upgrade, this breaking change will require some attention. Let's dive deep into what changed, why it changed, and how to handle it properly.

πŸ“‹ Table of Contents

What Changed?

In Next.js 14, searchParams was a simple object that you could access synchronously:

// βœ… Next.js 14 - Synchronous access
export default function SearchPage({ searchParams }) {
  const query = searchParams.q;
  const filter = searchParams.filter;

  return (
    <div>
      <h1>Search Results for: {query}</h1>
      <p>Filter: {filter}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In Next.js 15, searchParams becomes a Promise that must be awaited:

// βœ… Next.js 15 - Asynchronous access
export default async function SearchPage({ searchParams }) {
  const resolvedParams = await searchParams;
  const query = resolvedParams.q;
  const filter = resolvedParams.filter;

  return (
    <div>
      <h1>Search Results for: {query}</h1>
      <p>Filter: {filter}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why This Change Was Made

This change isn't arbitraryβ€”it's part of Next.js's broader strategy to improve performance and enable better streaming capabilities:

🎯 1. Streaming & Concurrent Rendering

By making searchParams async, Next.js can start rendering parts of your page before all search parameters are resolved, leading to faster perceived load times.

🎯 2. Better Resource Management

Async searchParams allows Next.js to handle URL parsing more efficiently, especially for complex query strings.

🎯 3. Consistency with React's Direction

This aligns with React's push toward async components and Suspense boundaries.

🎯 4. Future-Proofing

Sets the foundation for more advanced server-side optimizations in future releases.

Before vs After: Code Examples

Let's look at some practical examples to see the difference:

Simple Search Page

Next.js 14:

// app/search/page.js
export default function SearchPage({ searchParams }) {
  const { q, category, sort } = searchParams;

  return (
    <div className="container mx-auto p-4">
      <h1>Search Results</h1>
      {q && <p>Searching for: "{q}"</p>}
      {category && <p>Category: {category}</p>}
      {sort && <p>Sort by: {sort}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next.js 15:

// app/search/page.js
export default async function SearchPage({ searchParams }) {
  const { q, category, sort } = await searchParams;

  return (
    <div className="container mx-auto p-4">
      <h1>Search Results</h1>
      {q && <p>Searching for: "{q}"</p>}
      {category && <p>Category: {category}</p>}
      {sort && <p>Sort by: {sort}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

E-commerce Product Listing

Next.js 14:

// app/products/page.js
export default function ProductsPage({ searchParams }) {
  const page = parseInt(searchParams.page) || 1;
  const limit = parseInt(searchParams.limit) || 12;
  const sortBy = searchParams.sort || 'name';
  const filters = {
    brand: searchParams.brand,
    minPrice: searchParams.min_price,
    maxPrice: searchParams.max_price,
  };

  return (
    <div>
      <ProductList 
        page={page}
        limit={limit}
        sortBy={sortBy}
        filters={filters}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next.js 15:

// app/products/page.js
export default async function ProductsPage({ searchParams }) {
  const params = await searchParams;
  const page = parseInt(params.page) || 1;
  const limit = parseInt(params.limit) || 12;
  const sortBy = params.sort || 'name';
  const filters = {
    brand: params.brand,
    minPrice: params.min_price,
    maxPrice: params.max_price,
  };

  return (
    <div>
      <ProductList 
        page={page}
        limit={limit}
        sortBy={sortBy}
        filters={filters}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Migration Guide

Step 1: Make Your Page Component Async

// Before
export default function MyPage({ searchParams }) {
  // ...
}

// After
export default async function MyPage({ searchParams }) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Await searchParams

// Before
const { param1, param2 } = searchParams;

// After
const { param1, param2 } = await searchParams;
Enter fullscreen mode Exit fullscreen mode

Step 3: Update Any Helper Functions

If you have utility functions that process searchParams, they might need updates too:

// Before
function buildFilterObject(searchParams) {
  return {
    category: searchParams.category,
    priceRange: searchParams.price_range,
    inStock: searchParams.in_stock === 'true'
  };
}

// After
async function buildFilterObject(searchParams) {
  const params = await searchParams;
  return {
    category: params.category,
    priceRange: params.price_range,
    inStock: params.in_stock === 'true'
  };
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

❌ Pitfall 1: Forgetting to Await

// This will break in Next.js 15
export default async function Page({ searchParams }) {
  const query = searchParams.q; // Error: searchParams is a Promise
  return <div>{query}</div>;
}
Enter fullscreen mode Exit fullscreen mode

❌ Pitfall 2: Not Making Component Async

// This won't work
export default function Page({ searchParams }) {
  const params = await searchParams; // Error: await in non-async function
  return <div>...</div>;
}
Enter fullscreen mode Exit fullscreen mode

❌ Pitfall 3: Multiple Awaits

// Inefficient - awaiting multiple times
export default async function Page({ searchParams }) {
  const query = (await searchParams).q;
  const filter = (await searchParams).filter;
  const page = (await searchParams).page;
  // ...
}

// βœ… Better - await once, destructure
export default async function Page({ searchParams }) {
  const { q: query, filter, page } = await searchParams;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Destructure After Awaiting

export default async function Page({ searchParams }) {
  // βœ… Clean and efficient
  const { q, category, sort, page } = await searchParams;

  return (
    <div>
      <SearchResults 
        query={q}
        category={category}
        sortBy={sort}
        currentPage={parseInt(page) || 1}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Handle URL Parameter Validation

export default async function Page({ searchParams }) {
  const params = await searchParams;

  // Validate and provide defaults
  const page = Math.max(1, parseInt(params.page) || 1);
  const limit = Math.min(100, Math.max(1, parseInt(params.limit) || 10));
  const sortBy = ['name', 'price', 'date'].includes(params.sort) 
    ? params.sort 
    : 'name';

  return <ProductGrid page={page} limit={limit} sortBy={sortBy} />;
}
Enter fullscreen mode Exit fullscreen mode

3. Create Reusable Parameter Parsers

// lib/params.js
export async function parseSearchParams(searchParams) {
  const params = await searchParams;

  return {
    query: params.q || '',
    page: Math.max(1, parseInt(params.page) || 1),
    limit: Math.min(50, Math.max(1, parseInt(params.limit) || 10)),
    filters: {
      category: params.category,
      priceMin: parseFloat(params.price_min) || 0,
      priceMax: parseFloat(params.price_max) || Infinity,
      inStock: params.in_stock === 'true'
    }
  };
}

// app/products/page.js
import { parseSearchParams } from '@/lib/params';

export default async function ProductsPage({ searchParams }) {
  const { query, page, limit, filters } = await parseSearchParams(searchParams);

  return <ProductList {...{ query, page, limit, filters }} />;
}
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Let's build a complete search page that demonstrates the new async pattern:

// app/blog/page.js
import { BlogCard } from '@/components/BlogCard';
import { Pagination } from '@/components/Pagination';

export default async function BlogPage({ searchParams }) {
  // Await and destructure searchParams
  const {
    q: query,
    category,
    tag,
    page: pageParam,
    sort
  } = await searchParams;

  // Process parameters with validation
  const currentPage = Math.max(1, parseInt(pageParam) || 1);
  const postsPerPage = 10;
  const sortBy = ['date', 'title', 'views'].includes(sort) ? sort : 'date';

  // Build filter object
  const filters = {
    ...(query && { search: query }),
    ...(category && { category }),
    ...(tag && { tag }),
  };

  // Simulate API call (replace with your actual data fetching)
  const { posts, totalCount } = await fetchBlogPosts({
    filters,
    page: currentPage,
    limit: postsPerPage,
    sortBy
  });

  const totalPages = Math.ceil(totalCount / postsPerPage);

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <h1 className="text-4xl font-bold mb-4">Blog Posts</h1>

        {/* Display active filters */}
        {query && (
          <div className="mb-4">
            <span className="text-gray-600">Searching for: </span>
            <span className="font-semibold">"{query}"</span>
          </div>
        )}

        {(category || tag) && (
          <div className="flex gap-2 mb-4">
            {category && (
              <span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm">
                Category: {category}
              </span>
            )}
            {tag && (
              <span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm">
                Tag: {tag}
              </span>
            )}
          </div>
        )}
      </div>

      {/* Blog posts grid */}
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
        {posts.map(post => (
          <BlogCard key={post.id} post={post} />
        ))}
      </div>

      {/* Pagination */}
      {totalPages > 1 && (
        <Pagination 
          currentPage={currentPage}
          totalPages={totalPages}
          baseUrl="/blog"
          searchParams={filters}
        />
      )}
    </div>
  );
}

// Simulated data fetching function
async function fetchBlogPosts({ filters, page, limit, sortBy }) {
  // This would be your actual API call or database query
  // For demo purposes, we'll simulate it
  await new Promise(resolve => setTimeout(resolve, 100));

  return {
    posts: Array.from({ length: limit }, (_, i) => ({
      id: (page - 1) * limit + i + 1,
      title: "`Blog Post ${(page - 1) * limit + i + 1}`,"
      excerpt: "This is a sample blog post excerpt...",
      category: filters.category || 'General',
      publishedAt: new Date().toISOString(),
    })),
    totalCount: 95 // Simulated total
  };
}
Enter fullscreen mode Exit fullscreen mode

Performance Benefits

The async nature of searchParams enables several performance optimizations:

Streaming Benefits

export default async function Page({ searchParams }) {
  // This can start rendering immediately
  return (
    <div>
      <Header />
      <Suspense fallback={<SearchSkeleton />}>
        <SearchResults searchParams={searchParams} />
      </Suspense>
      <Footer />
    </div>
  );
}

async function SearchResults({ searchParams }) {
  const { q } = await searchParams; // Only this component waits
  const results = await fetchSearchResults(q);

  return <ResultsList results={results} />;
}
Enter fullscreen mode Exit fullscreen mode

Concurrent Data Fetching

export default async function Page({ searchParams, params }) {
  // Both can be resolved concurrently
  const [resolvedSearchParams, resolvedParams] = await Promise.all([
    searchParams,
    params
  ]);

  const { q } = resolvedSearchParams;
  const { slug } = resolvedParams;

  // Fetch data concurrently
  const [searchResults, pageData] = await Promise.all([
    fetchSearchResults(q),
    fetchPageData(slug)
  ]);

  return <CombinedView searchResults={searchResults} pageData={pageData} />;
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Support

For TypeScript users, here's how to properly type your async page components:

// types/page.ts
export interface SearchParams {
  q?: string;
  category?: string;
  page?: string;
  sort?: 'date' | 'title' | 'views';
  [key: string]: string | string[] | undefined;
}

// app/search/page.tsx
interface PageProps {
  searchParams: Promise<SearchParams>;
}

export default async function SearchPage({ searchParams }: PageProps) {
  const { q, category, page, sort } = await searchParams;

  // Type-safe parameter processing
  const currentPage = page ? Math.max(1, parseInt(page)) : 1;
  const sortBy: 'date' | 'title' | 'views' = 
    sort && ['date', 'title', 'views'].includes(sort) ? sort : 'date';

  return (
    <div>
      <SearchResults 
        query={q}
        category={category}
        page={currentPage}
        sortBy={sortBy}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Automated Migration

You can use a simple find-and-replace approach for basic migrations:

Find Pattern:

export default function.*\({ searchParams }\)
Enter fullscreen mode Exit fullscreen mode

Replace With:

export default async function $1({ searchParams }) {
  const resolvedSearchParams = await searchParams;
Enter fullscreen mode Exit fullscreen mode

Then update all references to searchParams to use resolvedSearchParams.

Testing Your Migration

Here's how to test that your migration works correctly:

// __tests__/search-page.test.js
import { render } from '@testing-library/react';
import SearchPage from '@/app/search/page';

// Mock searchParams as a resolved Promise
const mockSearchParams = Promise.resolve({
  q: 'test query',
  category: 'tech',
  page: '2'
});

test('renders search page with params', async () => {
  const { getByText } = render(
    await SearchPage({ searchParams: mockSearchParams })
  );

  expect(getByText('Searching for: "test query"')).toBeInTheDocument();
  expect(getByText('Category: tech')).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Error Handling

Always implement proper error handling when working with async searchParams:

export default async function Page({ searchParams }) {
  try {
    const params = await searchParams;
    const { q, filter } = params;

    return <SearchResults query={q} filter={filter} />;
  } catch (error) {
    console.error('Failed to resolve searchParams:', error);

    return (
      <div className="error-state">
        <h2>Something went wrong</h2>
        <p>Unable to load search parameters</p>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Quick Migration Checklist

  • [ ] Make page component async (async function)
  • [ ] Await searchParams before accessing properties
  • [ ] Update destructuring to happen after await
  • [ ] Test with different URL patterns
  • [ ] Update TypeScript types if using TypeScript
  • [ ] Update any utility functions that process searchParams
  • [ ] Add error handling for Promise rejection cases
  • [ ] Update tests to handle async components

Conclusion

The transition from synchronous to asynchronous searchParams in Next.js 15 represents a significant step forward in web performance optimization. While it requires some code changes, the benefits in terms of streaming, concurrent rendering, and overall performance make it worthwhile.

The migration is straightforward: make your components async, await searchParams, and you're good to go. The real benefits come from understanding how this change enables better user experiences through faster page loads and more responsive applications.

Key Takeaways:

  • πŸ”„ searchParams is now a Promise in Next.js 15
  • ⚑ This enables better streaming and performance
  • πŸ› οΈ Migration is simple: add async and await
  • 🎯 Focus on the performance benefits this unlocks
  • πŸ” Always validate and sanitize URL parameters

Have you started migrating to Next.js 15 yet? What other changes have you encountered? Share your experiences in the comments below!


Want to stay updated on Next.js developments? Follow me for more web development insights and tutorials!

Resources


#nextjs #react #webdev #javascript #frontend #migration

Top comments (0)