Type something to search...
Next.js useSearchParams

Next.js useSearchParams

Every URL in a web app carries two kinds of information: the path, and everything after the ?. Next.js gives you a clean way to read the path (route params, usePathname), but query strings are their own thing — filters, sort orders, pagination cursors, tracking parameters, all living in ?sort=asc&page=2. useSearchParams is the hook that hands you read access to that part of the URL from inside a Client Component, and it comes with a set of rendering rules that trip people up if you don't understand why they exist.

This isn't a hook you reach for casually. It has a hard requirement (Client Component only), a hard constraint during static rendering (Suspense boundaries), and a companion piece on the server side (the searchParams prop) that solves an overlapping but not identical problem. Getting the mental model right up front saves you from the single most common build-time error in the App Router: "Missing Suspense boundary with useSearchParams."

What useSearchParams actually returns

"use client";

import { useSearchParams } from "next/navigation";

export default function SearchBar() {
  const searchParams = useSearchParams();

  const search = searchParams.get("search");

  // URL -> /dashboard?search=my-project
  // search -> 'my-project'
  return <>Search: {search}</>;
}

It takes no arguments and returns a read-only URLSearchParams object — the same interface the browser's native URLSearchParams implements, just with mutation methods stripped off. You get get(), has(), getAll(), keys(), values(), entries(), forEach(), and toString(), but nothing that lets you call .set() or .delete() directly on the object the hook returns.

A few behaviors of get() are worth internalizing early, because they're the source of subtle bugs:

URLsearchParams.get("a")
/dashboard?a=1'1'
/dashboard?a='' (empty string, not null)
/dashboard?b=3null
/dashboard?a=1&a=2'1' — only the first value

That last row matters more than it looks. If your UI supports multi-select filters (?tag=react&tag=nextjs), reaching for .get() will silently drop everything but the first tag. You want .getAll('tag') there instead, and it's an easy thing to miss during a code review since .get() and .getAll() look almost interchangeable at a glance.

Everything the hook returns is read-only by design. There's no useSearchParams() equivalent of a setSearchParams setter the way you might expect from a state-management library. Changing the query string means changing the URL — which is the whole point, and which is where useRouter and Link come back into the picture.

Why this is a Client Component hook, not a Server one

useSearchParams is explicitly unsupported in Server Components. The docs frame this as preventing "stale values during partial rendering," which is worth unpacking: in the App Router, layouts persist across navigations by default, and only the parts of the tree that actually change get re-rendered. If a Server Component layout could read query params directly, it would capture whatever the params were at the moment of its last render — and then just sit there showing stale data on a subsequent client-side navigation that only changed the query string. Client Components, by contrast, re-render in the browser on every navigation, so they always see the current params.

If you're in a Server Component and need query-string data, the docs are direct about the right tool: read the searchParams prop on the Page component instead, and pass it down as a prop to whatever Server or Client children need it. That prop is inherently tied to the request being served, so it can never go stale the way a hook call inside a persisted layout could.

One asymmetry worth flagging: Pages receive a searchParams prop, but Layouts do not — deliberately, for the exact staleness reason above. A shared layout isn't re-rendered on every navigation, so if it accepted a searchParams prop, that prop would freeze at whatever value existed when the layout last rendered. If you need query-string-driven behavior inside a layout, your two options are the Page's searchParams prop (passed down as a regular prop) or useSearchParams inside a Client Component nested in that layout.

The Suspense requirement, and why it exists

This is the part of useSearchParams that actually causes build failures, so it deserves the most attention.

When a route is statically prerendered, Next.js builds the HTML for it once, ahead of any request — which means there's no incoming URL to read query params from at build time. Calling useSearchParams in that context forces the Client Component subtree up to the nearest Suspense boundary to fall back to client-side rendering, since the actual value simply doesn't exist until a real request lands in the browser.

import { Suspense } from "react";
import SearchBar from "./search-bar";

function SearchBarFallback() {
  return <>placeholder</>;
}

export default function Page() {
  return (
    <>
      <nav>
        <Suspense fallback={<SearchBarFallback />}>
          <SearchBar />
        </Suspense>
      </nav>
      <h1>Dashboard</h1>
    </>
  );
}

Wrap the component that calls useSearchParams in a Suspense boundary, and only that component client-renders — everything else in the tree, including sibling Client Components, still gets prerendered and shipped as static HTML. Skip the boundary, and a production build fails outright with the "Missing Suspense boundary with useSearchParams" error, because Next.js has nowhere to draw the line between what can be static and what can't.

Here's the trap: in development, this doesn't happen. Dev mode renders routes on demand rather than ahead of time, so useSearchParams never needs to suspend, and code that's missing a Suspense boundary will work perfectly on next dev and then fail the moment you run next build. If you've ever shipped a change that passed local testing and then broke CI, this is one of the more common causes — always run a production build before trusting that a page using this hook is actually safe.

If the route is dynamically rendered instead of prerendered, none of this applies — useSearchParams is available on the server during the component's initial render, same as any other prop, because there's a real request to read params from. The recommended way to force that dynamic behavior is calling connection() in a Server Component ancestor, which the docs now prefer over the older export const dynamic = 'force-dynamic' route segment config, since connection() ties the dynamic behavior explicitly to "wait for the incoming request" rather than a blanket opt-out of static generation.

Updating search params without a full page reload

Since the returned object is read-only, updating the query string means constructing a new URL and navigating to it — but because this stays client-side, it doesn't trigger a full page reload the way changing window.location would.

"use client";

import { useCallback } from "react";
import { usePathname, useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";

export default function ExampleClientComponent() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const createQueryString = useCallback(
    (name: string, value: string) => {
      const params = new URLSearchParams(searchParams.toString());
      params.set(name, value);
      return params.toString();
    },
    [searchParams],
  );

  return (
    <>
      <button
        onClick={() => {
          router.push(pathname + "?" + createQueryString("sort", "asc"));
        }}
      >
        Sort Ascending
      </button>

      <Link href={pathname + "?" + createQueryString("sort", "desc")}>
        Sort Descending
      </Link>
    </>
  );
}

The pattern is consistently: copy the current params into a mutable URLSearchParams instance, make the change there, stringify it, and either router.push() the new URL or hand it to a <Link href>. Two details in that snippet matter more than they look:

  • Copy, don't mutate. new URLSearchParams(searchParams.toString()) creates a fresh, writable object from the read-only one — you can't call .set() directly on what the hook returns.
  • Prefer Link over router.push() when the destination is knowable at render time. A Link gets prefetching for free; an onClick handler calling router.push() doesn't unless you set it up separately. Reserve the button/router.push() pattern for cases where the new query string genuinely depends on something computed inside the handler (a form value, a debounce timer, a confirmation step) rather than something known while rendering.

After a navigation completes this way, the current Page's searchParams prop updates with the new values too — so any Server Component reading from that prop stays in sync, even though the actual navigation was driven entirely from client-side code.

Common mistakes

Forgetting the Suspense boundary and only finding out in CI. Covered above, but worth repeating as the single most frequent failure mode: this passes locally in dev every time and only breaks on next build.

Reaching for .get() on a multi-value param. If your UI allows selecting more than one value for the same key, .get() quietly returns only the first one. Use .getAll() when duplicates are expected.

Trying to read search params from a layout. Layouts don't receive a searchParams prop at all, and for good reason — they aren't re-rendered on every navigation, so that prop would go stale. If a layout needs query-string data, push the read down into a Client Component using useSearchParams, or restructure so the data flows from the Page down as a prop instead.

Mutating the returned object directly. params.set(...) on the object useSearchParams() gives you will throw, because it's genuinely read-only. Always wrap it in new URLSearchParams(...) first if you need to build a modified version.

Using force-dynamic out of habit instead of connection(). The route segment config still works, but the docs now steer you toward connection() as the more semantically precise way to say "this route depends on the incoming request" — it reads better in code review and ties the opt-out to a concrete reason rather than a blanket flag.

Key Takeaways

QuestionAnswer
Where can I call useSearchParams?Client Components only — never Server Components
What does it return?A read-only URLSearchParams (get, has, getAll, etc.)
Static route + this hook?Requires a Suspense boundary, or the production build fails
Does dev mode catch missing Suspense boundaries?No — only next build does; always verify with a production build
Need search params in a Server Component Page?Use the searchParams prop instead
Need them in a layout?Not directly — layouts don't get searchParams; push the read into a Client Component
How do I update params?Build a new URLSearchParams, stringify it, navigate with router.push() or Link
Multiple values for one key?Use .getAll(), not .get()

useSearchParams is a small API surface with an outsized number of rules attached to it, but every rule traces back to one idea: query params belong to a specific request, and the App Router's caching and prerendering model needs to know exactly which parts of your UI depend on that request so it can decide what's safe to build ahead of time. Once that clicks, the Suspense boundary requirement stops looking like boilerplate and starts looking like the framework being honest about what it can and can't know before your app is actually running.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper