Search Params
Read and manipulate URL query parameters with useSearchParams (client) and the searchParams prop (server).
Search across all documentation pages
Read and manipulate URL query parameters with useSearchParams (client) and the searchParams prop (server).
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card -- copy-paste ready.
// Server Component -- access searchParams as a prop (Promise in Next.js 15+)
type Props = { searchParams: Promise<{ q?: string; page?: string }> };
export default async function Page({ searchParams }: Props) {
const { q, page } = await searchParams;
const results = await search(q, Number(page) || 1);
return <ResultsList results={results} />;
}// Client Component -- useSearchParams hook
"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
export function SearchInput() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams.toString());
if (term) params.set("q", term);
else params.delete("q");
router.push(`${pathname}?${params.toString()}`);
}
return (
<input
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
);
}When to reach for this: You need URL-driven state -- search filters, pagination, sorting -- that should be shareable via URL and survive page refreshes.
// app/products/page.tsx (Server Component)
import { Suspense } from "react";
import { SearchBar } from "./search-bar";
import { ProductGrid } from "./product-grid";
import { Pagination } from "./pagination";
type Props = {
searchParams: Promise<{
q?: string;
category?: string;
sort?: string;
page?: string;
}>;
};
export default async function ProductsPage({ searchParams }: Props) {
const { q, category, sort, page } = await searchParams;
return (
<main className="max-w-6xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Products</h1>
<Suspense fallback={<div>Loading search...</div>}>
<SearchBar />
</Suspense>
<Suspense
key={`${q}-${category}-${sort}-${page}`}
fallback={<div>Loading products...</div>}
>
<ProductResults
query={q}
category={category}
sort={sort ?? "newest"}
page={Number(page) || 1}
/>
</Suspense>
</main>
);
}
async function ProductResults({
query,
category,
sort,
page,
}: {
query?: string;
category?: string;
sort: string;
page: number;
}) {
const { products, totalPages } = await fetchProducts({
query,
category,
sort,
page,
});
return (
<>
<ProductGrid products={products} />
<Pagination currentPage={page} totalPages={totalPages} />
</>
);
}// app/products/search-bar.tsx
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
export function SearchBar() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams.toString());
if (term) {
params.set("q", term);
params.set("page", "1"); // reset to page 1 on new search
} else {
params.delete("q");
}
startTransition(() => {
router.push(`${pathname}?${params.toString()}`);
});
}, 300);
return (
<div className="relative mb-6">
<input
type="search"
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search products..."
className="w-full border rounded px-4 py-2"
/>
{isPending && (
<span className="absolute right-3 top-2.5 text-gray-400">
Searching...
</span>
)}
</div>
);
}// app/products/pagination.tsx
"use client";
import { useSearchParams, usePathname } from "next/navigation";
import Link from "next/link";
export function Pagination({
currentPage,
totalPages,
}: {
currentPage: number;
totalPages: number;
}) {
const searchParams = useSearchParams();
const pathname = usePathname();
function createPageUrl(page: number) {
const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString());
return `${pathname}?${params.toString()}`;
}
return (
<div className="flex gap-2 mt-6">
{currentPage > 1 && (
<Link href={createPageUrl(currentPage - 1)} className="px-3 py-1 border rounded">
Previous
</Link>
)}
<span className="px-3 py-1">
Page {currentPage} of {totalPages}
</span>
{currentPage < totalPages && (
<Link href={createPageUrl(currentPage + 1)} className="px-3 py-1 border rounded">
Next
</Link>
)}
</div>
);
}What this demonstrates:
searchParams on the server as a Promise (Next.js 15+ pattern)useSearchParams on the client to read and update query parameterskey on <Suspense> to re-trigger loading states on param changessearchParams: In the App Router, page components receive searchParams as a prop. In Next.js 15+, this is a Promise that must be awaited. Accessing searchParams opts the route into dynamic rendering.useSearchParams: Returns a read-only URLSearchParams instance. To update, construct a new URLSearchParams, modify it, and push with router.push() or router.replace().useSearchParams requires a Suspense boundary -- During static prerendering, search params are not available. Wrapping the component in <Suspense> provides a fallback while params hydrate on the client.router.push creates a new history entry; router.replace replaces the current entry (better for filters and sorting).Using router.replace to avoid polluting history:
// Good for filters -- user can still use back button meaningfully
startTransition(() => {
router.replace(`${pathname}?${params.toString()}`);
});Reading a single param with a default:
const sort = searchParams.get("sort") ?? "newest";
const page = Number(searchParams.get("page")) || 1;Multi-value params (arrays):
// URL: ?color=red&color=blue
const colors = searchParams.getAll("color"); // ["red", "blue"]
// Setting multiple values
const params = new URLSearchParams();
["red", "blue"].forEach((c) => params.append("color", c));// Server Component searchParams type (Next.js 15+)
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// Typed search params helper
type ProductFilters = {
q?: string;
category?: string;
sort?: "newest" | "price-asc" | "price-desc";
page?: string;
};
type Props = { searchParams: Promise<ProductFilters> };searchParams opts the route into dynamic rendering -- Any page that reads searchParams on the server cannot be statically generated. Fix: If you want a static page with client-side filtering, read params only in Client Components with useSearchParams.
useSearchParams without Suspense causes build errors -- During prerendering, useSearchParams throws because there are no params to read. Fix: Always wrap components using useSearchParams in a <Suspense> boundary.
Stale closure with searchParams -- In a Client Component, the searchParams reference from useSearchParams() updates on navigation, but closures in event handlers may capture the old value. Fix: Read searchParams at call time inside the handler, not outside it.
Array params lose type safety -- searchParams.get("color") returns only the first value even when multiple exist. Fix: Use searchParams.getAll("color") for multi-value params.
searchParams is a Promise in Next.js 15+ -- Destructuring it directly without await gives you a Promise object. Fix: Always const { q } = await searchParams in Server Components.
| Alternative | Use When | Don't Use When |
|---|---|---|
useState (client-only) | State is ephemeral and does not need to survive refresh | State should be shareable via URL |
Dynamic route segments ([slug]) | The parameter defines the resource identity | The parameter is a filter or modifier |
| Cookies | State should persist across sessions but not appear in the URL | State should be visible and shareable |
nuqs library | You want type-safe, validated search params with serialization | Built-in useSearchParams is sufficient |
| Zustand with URL sync | Complex multi-param state with derived values | Simple key-value URL params |
searchParams is a Promise that must be awaited before accessing propertiesawait gives you a Promise object, not the valuessearchParams in a Server Component opts the route into dynamic renderinguseSearchParams<Suspense> boundary, the build fails because useSearchParams throwsrouter.push creates a new browser history entry (user can press back)router.replace replaces the current history entry (better for filters and sorting)replace when frequent param changes would pollute the history stack// Reading multiple values
const colors = searchParams.getAll("color"); // ["red", "blue"]
// Setting multiple values
const params = new URLSearchParams();
["red", "blue"].forEach((c) => params.append("color", c));searchParams.get("color") only returns the first valueconst handleSearch = (term: string) => {
const params = new URLSearchParams(searchParams.toString());
if (term) {
params.set("q", term);
params.set("page", "1"); // reset to page 1
} else {
params.delete("q");
}
router.push(`${pathname}?${params.toString()}`);
};type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
};string, string[], or undefinedsearchParams referencesearchParams object updates on navigation, but the closure does not re-capture itsearchParams at call time inside the handler to get the latest value<Suspense
key={`${q}-${category}-${sort}-${page}`}
fallback={<div>Loading...</div>}
>
<ProductResults query={q} sort={sort} page={page} />
</Suspense>key unmounts and remounts the Suspense boundary, showing the fallback again[slug]) when the parameter defines the resource identity (e.g., /products/shoes)searchParams when the parameter is a filter or modifier (e.g., ?sort=price&page=2)type ProductFilters = {
q?: string;
category?: string;
sort?: "newest" | "price-asc" | "price-desc";
page?: string;
};
type Props = { searchParams: Promise<ProductFilters> };useSearchParams livesReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥