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
searchParamsinto 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(). UsegenerateMetadataand route params, or handle query-driven canonicalization carefully. - If you need to fetch data based on query params, prefer server-side
searchParamsviaPage({ 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
searchParamsto render filtered results when needed).