useSearchParams()

Read the current URL's query string as a read-only URLSearchParams object on the client.

Signature
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 searchParams prop of page.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; use getAll().
  • If you want the route dynamic instead of client-rendered, await connection() in a parent Server Component rather than reaching for export const dynamic.

Example

Example · typescript
'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.

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
'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

  • Be the first to comment on this lesson.