javal.ge
javascript

Next.js App Router: Fix "useSearchParams() should be wrapped in a suspense boundary" Without Breaking SEO

// app/products/page.tsx (Server Component)
import { Suspense } from "react";
import FiltersFromQuery from "./FiltersFromQuery";

export default async function ProductsPage() {
  // Server-rendered, SEO-critical content stays here
  const products = await fetch("https://example.com/api/products", {
    cache: "no-store",
  }).then((r) => r.json());

  return (
    <main>
      <h1>Products</h1>

      {/* Only the query-dependent UI is client-rendered */}
      <Suspense fallback={<div>Filters: All</div>}>
        <FiltersFromQuery />
      </Suspense>

      <ul>
        {products.map((p: any) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </main>
  );
}

// app/products/FiltersFromQuery.tsx (Client Component)
"use client";

import { useMemo } from "react";
import { useSearchParams } from "next/navigation";

export default function FiltersFromQuery() {
  const sp = useSearchParams();
  const category = sp.get("category");
  const sort = sp.get("sort") ?? "relevance";

  const label = useMemo(() => {
    if (!category) return `Filters: All (sort: ${sort})`;
    return `Filters: ${category} (sort: ${sort})`;
  }, [category, sort]);

  return <div>{label}</div>;
}

Why this warning happens (and why it matters)

  • useSearchParams() is a client-only hook. In the App Router, reading URL search params forces a client boundary.
  • During pre-render/SSR, Next can’t resolve client navigation state, so it requires <Suspense> to avoid hydration mismatches.
  • Many “quick fixes” move the entire page to "use client", which can hurt SEO and performance by pushing everything to the client.

Goal: keep the page server-rendered, isolate only the params reader

  • Keep your route segment/page as a Server Component for SEO, streaming, caching, and smaller JS payloads.
  • Move only the logic that reads searchParams into a small Client Component.
  • Wrap that component in <Suspense> with a minimal fallback.

Implementation pattern (recommended)

  • Create a Server Component page that renders SEO-critical content normally.
  • Import a Client Component that uses useSearchParams().
  • Wrap it in <Suspense> so Next can stream HTML while the client hydrates the params-dependent UI.

Common pitfalls

  • Don’t put "use client" at the top of the page unless you truly need client state for the whole page.
  • Don’t compute canonical/metadata based on useSearchParams(). Use generateMetadata and route params, or handle query-driven canonicalization carefully.
  • If you need to fetch data based on query params, prefer server-side searchParams via Page({ searchParams }) (when supported) and only use the client hook for UI affordances (filters, tabs, highlighting).

SEO-safe fallback strategy

  • Make the Suspense fallback render neutral UI (e.g., “All results”) so crawlers and users see meaningful content immediately.
  • Ensure the server-rendered content is valid without query params (or use server searchParams to render filtered results when needed).
Back to Tips

🍪 Cookie Consent

We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept", you consent to our use of cookies.