How to Fix: Pages using searchParams do not cache

6 min read

When a Next.js App Router page reads searchParams, it often stops behaving like a statically cached page. The result is confusing: routes that look cacheable suddenly render dynamically, causing misses in the Full Route Cache and making production behavior differ from expectations.

This issue appears in reproductions like the one linked in the GitHub report, where a page depends on query string values and is expected to cache after build, but does not. In practice, the behavior makes sense once you understand how App Router rendering modes work.

Understanding the Root Cause

In the Next.js App Router, caching is tied to whether a route can be treated as static or must remain dynamic. A page that depends on request-specific data cannot safely be fully cached at build time unless Next.js can determine every possible output ahead of time.

searchParams are request-time inputs. Even if the rest of the page is static, reading query parameters tells Next.js that the rendered result may change for each incoming URL, such as ?page=1, ?page=2, or ?sort=asc. Because of that, the framework does not place the page into the same route-level static cache used for fully deterministic pages.

There are three important layers to separate:

  • Full Route Cache: stores rendered output for static routes.
  • Data Cache: stores cached results from fetch() when configured for caching.
  • Request-time rendering: executes the page on demand when the route depends on request state like headers, cookies, or query params.

The key point is this: using searchParams affects route rendering mode, not necessarily fetch caching. Your page may still benefit from cached data requests, but the page HTML itself is typically not cached as a fully static route when query parameters influence rendering.

This is why the issue shows up after running a production build. Developers often expect that if the page does not use cookies or headers, it should still cache. But once the page output varies by URL query string, Next.js cannot assume a single stable HTML artifact for that route.

In other words, the page is not broken; it is being classified as dynamic by design.

Step-by-Step Solution

The fix depends on what you actually want to cache:

  1. If you want the entire page HTML cached, do not make the server-rendered page depend on searchParams.
  2. If you need query params for filtering or sorting, move that logic to the client or cache the underlying data fetches instead of the whole route.
  3. If a finite set of query-driven views should be static, convert them into path segments or pre-generated params rather than query strings.

1. Keep the route static by removing server dependency on searchParams

If query params are only used for UI behavior, read them in a Client Component with useSearchParams() and keep the main page static.

// app/products/page.tsx
import ProductFilters from './ProductFilters'

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache',
  }).then((res) => res.json())

  return (
    <div>
      <h1>Products</h1>
      <ProductFilters products={products} />
    </div>
  )
}
// app/products/ProductFilters.tsx
'use client'

import { useSearchParams } from 'next/navigation'

export default function ProductFilters({ products }) {
  const searchParams = useSearchParams()
  const sort = searchParams.get('sort') || 'name'

  const sortedProducts = [...products].sort((a, b) => {
    if (sort === 'name') return a.name.localeCompare(b.name)
    return a.price - b.price
  })

  return (
    <ul>
      {sortedProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

This works best when the query string changes presentation but not the core server-rendered payload.

2. Cache the data, not the route

If the page must use searchParams on the server, make peace with dynamic rendering and cache the expensive data requests.

// app/search/page.tsx
export default async function SearchPage({
  searchParams,
}: {
  searchParams: { q?: string }
}) {
  const q = searchParams.q || ''

  const results = await fetch(`https://api.example.com/search?q=${encodeURIComponent(q)}`, {
    cache: 'force-cache',
    next: { revalidate: 300 },
  }).then((res) => res.json())

  return (
    <div>
      <h1>Results for: {q}</h1>
      <pre>{JSON.stringify(results, null, 2)}</pre>
    </div>
  )
}

Here, the route is still request-driven, but the fetched data can be reused according to the revalidation policy.

3. Replace query params with static path segments

If you know the valid variants ahead of time, move them into the URL path and use generateStaticParams.

// app/products/sort/[order]/page.tsx
export function generateStaticParams() {
  return [{ order: 'name' }, { order: 'price' }]
}

export default async function ProductsSortedPage({
  params,
}: {
  params: { order: string }
}) {
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache',
  }).then((res) => res.json())

  const sortedProducts = [...products].sort((a, b) => {
    if (params.order === 'name') return a.name.localeCompare(b.name)
    return a.price - b.price
  })

  return (
    <ul>
      {sortedProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

This allows Next.js to pre-render known route variants and place them into the static route cache.

4. Verify what is actually dynamic

During debugging, inspect whether the page is dynamic because of searchParams alone or because another API such as cookies(), headers(), or uncached fetch() is also involved.

// Avoid mixing these into pages you expect to be static
import { cookies, headers } from 'next/headers'

export default async function Page({ searchParams }) {
  // searchParams already pushes the route toward request-time behavior
  const cookieStore = cookies()
  const headerStore = headers()

  return <div>Debug route behavior</div>
}

If any request-bound API is used, the route will stay dynamic regardless of other caching options.

5. Use a clear rendering strategy

Choose one of these patterns instead of mixing assumptions:

  • Static page + client-side query handling
  • Dynamic page + cached fetches
  • Static path-based variants

That architectural decision is the real solution to this GitHub issue.

Common Edge Cases

  • Query params only affect a tiny component: If only a small widget depends on the URL, isolate it in a Client Component instead of passing searchParams through the server page.
  • Uncached fetch makes everything feel dynamic: Even without searchParams, a fetch() configured with cache: 'no-store' or dynamic request data can prevent useful caching.
  • Mixing searchParams with cookies or headers: This guarantees request-time rendering and often leads developers to blame query params alone.
  • Expecting one cache entry per query string in Full Route Cache: Next.js does not treat query-driven server pages like prebuilt static pages for arbitrary URL combinations.
  • SEO-sensitive filtered pages: If filtered states must be crawlable and cached, path segments are usually better than query strings.
  • Hydration mismatch after moving logic client-side: If the server renders one default sort order and the client immediately applies another based on the URL, the UI may appear to change after hydration. Render a stable fallback or pass initial state carefully.

FAQ

Does using searchParams always disable all caching in Next.js?

No. It usually prevents full static route caching for that page, but data fetched inside the page can still be cached using fetch() cache settings and revalidation options.

Why does the page work in development but behave differently after build?

Development mode does not reflect production caching behavior accurately. The real distinction between static and dynamic rendering becomes much clearer after next build and next start.

What is the best fix if I need SEO-friendly pages for filtered content?

Use path segments instead of query params for important variants, and pre-render them with generateStaticParams when the set is known. That gives you a much better match for static caching and search engine indexing.

The core takeaway is simple: searchParams are request-time input. If a page reads them on the server, Next.js treats the route as dynamic unless you redesign the route so the variable part lives in the client or in pre-generated path params. Once you align your implementation with that rule, the caching behavior becomes predictable.

Leave a Reply

Your email address will not be published. Required fields are marked *