useSearchParams()
Read the current URL's query string as a read-only URLSearchParams object on the client.
import { useSearchParams } from 'next/navigation'
const searchParams = useSearchParams()- Added in Next.js v13.0.0
Return value
ReadonlyURLSearchParamsA read-only URLSearchParams for the current query string, exposing get(), getAll(), has(), keys(), values(), entries(), forEach() and toString(). In a project that also has a pages directory the type is ReadonlyURLSearchParams | null.
Errors
A production build fails with "Missing Suspense boundary with useSearchParams" when a statically prerendered route calls the hook outside a Suspense boundary; calling it in a Server Component is unsupported.
Changelog
- PHP v13.0.0useSearchParams introduced. It replaces the query object of the Pages Router's useRouter.
useSearchParams gives a Client Component the query string as a read-only URLSearchParams. Use it for state you want to live in the URL — filters, sorting, pagination, a search box, the open tab — so the state is shareable, bookmarkable, and survives a reload. Writing is a separate step: build a new URLSearchParams from toString(), mutate the copy, and hand it to router.push or a <Link>.
The Suspense rule
This is the hook people file bugs about. On a prerendered route, calling it makes the whole Client Component tree up to the nearest <Suspense> boundary render on the client only. In development routes are rendered on demand, so it appears to work; the production build then fails outright. Wrap the component that calls the hook in <Suspense> with a fallback so everything above it can still be prerendered and streamed in the initial HTML.
What trips people up
- The object is read-only.
searchParams.set(...)is not available; copy it first. - On a Server Component page, prefer the
searchParamsprop ofpage.js— it is a Promise you await. Layouts never receive that prop, which is exactly when this hook is the right answer. get()returns only the first value for a repeated key; usegetAll().- If you want the route dynamic instead of client-rendered,
await connection()in a parent Server Component rather than reaching forexport const dynamic.
Example
'use client'
import { useCallback } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
export default function SortSelect() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// URL -> /products?sort=desc
// sort -> 'desc'
const sort = searchParams.get('sort') ?? 'asc'
const setSort = useCallback(
(value: string) => {
// The returned object is read-only: copy it before mutating
const params = new URLSearchParams(searchParams.toString())
params.set('sort', value)
router.push(`${pathname}?${params.toString()}`, { scroll: false })
},
[pathname, router, searchParams]
)
return (
<select value={sort} onChange={(event) => setSort(event.target.value)}>
<option value="asc">Price: low to high</option>
<option value="desc">Price: high to low</option>
</select>
)
}When to use it
- Read ?search= into a controlled search input that survives a page reload.
- Toggle a sort order by pushing an updated query string with useRouter.
- Read a ?tab= parameter in a layout, which never receives the searchParams prop.
- Preserve existing filters while changing the page number in a paginated list.
Frequently asked questions
How do I fix "Missing Suspense boundary with useSearchParams"?
Wrap the Client Component that calls useSearchParams in a <Suspense> boundary with a fallback. The hook forces client-side rendering up to the nearest boundary, and a statically prerendered route with no boundary fails the production build.
How do I update the query string without reloading the page?
Copy the params with new URLSearchParams(searchParams.toString()), set your key, then call router.push(`${pathname}?${params}`). The returned object is read-only, so mutating it directly does nothing.
Can I use useSearchParams in a Server Component?
No. Use the searchParams prop of page.js, awaiting it in Next.js 15+, and pass the values down as props. Layouts never receive searchParams, so a layout must use the hook in a Client Component.
More examples
The Suspense boundary the build demands
The fallback is what ships in the prerendered HTML; React swaps in the real SortSelect once the query string is known during hydration. Without this boundary, `next build` stops with "Missing Suspense boundary with useSearchParams" even though `next dev` was fine.
// app/products/page.tsx
import { Suspense } from 'react'
import SortSelect from './sort-select'
function SortSelectFallback() {
return <select disabled aria-hidden />
}
export default function Page() {
return (
<>
<h1>Products</h1>
<Suspense fallback={<SortSelectFallback />}>
<SortSelect />
</Suspense>
{/* the rest of the page is still prerendered */}
</>
)
}Reading search params on the server instead
When the query string drives data fetching, the page's searchParams prop is the better tool: it needs no Suspense boundary and keeps the work on the server. Since Next.js 15 the prop is a Promise and must be awaited. Layouts do not get this prop, which is why a layout still needs the hook.
// app/products/page.tsx — Server Component, Next.js 15+
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { sort = 'asc', q } = await searchParams
const products = await getProducts({ sort, query: q })
return <ProductList products={products} />
}Repeated keys and iteration
get() collapses a repeated key to its first value, which silently drops filters in multi-select UIs; getAll() returns every value. has() distinguishes ?min= (empty string) from an absent key, where get() returns '' and null respectively.
'use client'
import { useSearchParams } from 'next/navigation'
export default function ActiveFilters() {
const searchParams = useSearchParams()
// URL -> /shop?tag=new&tag=sale&min=10
const tags = searchParams.getAll('tag') // ['new', 'sale']
const first = searchParams.get('tag') // 'new'
const hasMin = searchParams.has('min') // true
return (
<ul>
{tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
{hasMin ? <li>min {searchParams.get('min')}</li> : null}
</ul>
)
}
Discussion