Next.js Query String Params: searchParams + useRouter
If `router.query` examples keep breaking on you, the problem is usually that you're mixing App Router and Pages Router APIs. Here is the exact fix for each case.
The symptom is simple: you open /dashboard?search=invoice&page=2, copy an old snippet, and get undefined, stale values, or the wrong API entirely.
The root cause is that Next.js now has two routing models, and the correct query-string API depends on where you read the params:
- App Router Server Component page: use the
searchParamsprop - App Router Client Component: use
useSearchParams() - Shared client component across both routers:
useSearchParams()still works
Here is the exact fix for each case.
The App Router server-side fix#
If you are inside app/.../page.tsx, use the page prop. In the current Next.js docs, searchParams is a promise in modern App Router pages.
// app/dashboard/page.tsx
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { search = '', page = '1' } = await searchParams
return (
<main>
<h1>Dashboard</h1>
<p>Search: {search}</p>
<p>Page: {page}</p>
</main>
)
}Use this when the query string affects data fetching, pagination, filtering, or metadata for the page itself.
The App Router client-side fix#
If the component is interactive and already marked 'use client', use useSearchParams() from next/navigation.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchSummary() {
const searchParams = useSearchParams()
const search = searchParams.get('search') ?? ''
const page = searchParams.get('page') ?? '1'
return (
<p>
Searching for <strong>{search || 'everything'}</strong> on page {page}
</p>
)
}For a statically rendered route, the parent page must provide a Suspense boundary.
The Next.js hook documentation
explains that this is required for the production build, even when development appears to work:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import SearchSummary from './search-summary'
export default function Page() {
return (
<Suspense fallback={<p>Loading search...</p>}>
<SearchSummary />
</Suspense>
)
}Save the client component above as app/dashboard/search-summary.tsx. The same parent
boundary is needed when the shared SearchBar below is used on a static App Router page.
If production reports the missing boundary, follow the
useSearchParams Suspense fix.
Two further details matter:
useSearchParams()is read-only.- In the App Router docs, Next.js explicitly recommends the page
searchParamsprop if you are already in a Server Component page.
The shared-component pattern that survives both routers#
This is the cleanest answer if you are migrating gradually or sharing a search bar between pages/ and app/.
'use client'
import { useSearchParams } from 'next/navigation'
export function SearchBar() {
const searchParams = useSearchParams()
if (!searchParams) {
return <input defaultValue="" placeholder="Search..." />
}
const search = searchParams.get('search') ?? ''
return <input defaultValue={search} placeholder="Search..." />
}That fallback is important in Pages Router prerendering. The official Pages Router docs now document useSearchParams from next/navigation as a shared option across both routers.
The mistake that keeps causing confusion#
Most search results still mix old Pages Router answers with new App Router code.
If you copied something like this into app/page.tsx:
// Wrong mental model for a modern App Router page
const query = router.query.searchyou are using the wrong abstraction. In the App Router, the page itself should usually read searchParams, and client components should use useSearchParams.
Which API should you choose?#
Use the page prop when:
- the param changes the server query
- the param affects SEO or pagination
- the page should render from the URL alone
Use useSearchParams() when:
- the component is client-only
- you need live access after client navigation
- you are building a reusable search or filter widget
The production-safe rule#
If the URL controls what data the page loads, read it on the server first and pass stable values downward. That keeps your route easy to reason about and avoids a lot of hydration and loading-state noise.
If you are already untangling router differences, these are worth reading next:
- How to Access Route Parameter Inside getServerSideProps
- Next.js 15 Middleware: Complete Guide to Auth, Rate Limiting, A/B Testing, and Edge Logic
- Fix "cookies() should be awaited" Error in Next.js 15
References#
- Stack Overflow: How can I get (query string) parameters from the URL in Next.js?
- Next.js docs: useSearchParams
- Next.js docs: page.js
searchParams - Next.js docs: useSearchParams in the Pages Router
Reading query params in a Route Handler#
None of the fixes above apply inside app/api/.../route.ts. A Route Handler is not a
page — it receives no searchParams prop, and it isn't a component, so useSearchParams
doesn't apply either. The query string arrives as part of the incoming Request, and the
documented way to read it is request.nextUrl.searchParams, which is a standard
URLSearchParams instance:
// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const query = searchParams.get('q') ?? ''
const page = searchParams.get('page') ?? '1'
return NextResponse.json({ query, page })
}request.nextUrl is a Next.js extension of the standard URL object attached to
NextRequest, so every URLSearchParams method — .get(), .getAll(), .has() — works
exactly as it does on the client. This is also the point where a lot of teams debate
whether a query-driven endpoint belongs in a Route Handler at all versus a Server Action;
Next.js Server Actions vs API Routes: When to Use Each
covers that tradeoff directly — the short version is that a Route Handler is the right
choice specifically when an external client (not just your own app's forms) needs to call
the endpoint through a stable HTTP contract. Server Actions use a framework-specific POST
protocol and are directly reachable over HTTP too: they must check authentication and
authorisation inside the action, as the Next.js security guide
requires. Hiding the calling button does not protect the action.
Reading query params in Middleware#
Middleware runs before a request reaches any page or Route Handler,
and it has the exact same request.nextUrl.searchParams shape as a Route Handler — not
the searchParams page prop, and not useSearchParams, since Middleware isn't part of
the React render tree at all:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const ref = request.nextUrl.searchParams.get('ref')
if (ref === 'legacy-campaign') {
const url = request.nextUrl.clone()
url.searchParams.delete('ref')
url.searchParams.set('utm_source', 'legacy-campaign')
return NextResponse.redirect(url)
}
return NextResponse.next()
}This is the pattern behind rewriting or redirecting based on a tracking parameter, an A/B
test bucket, or a feature flag stored in the query string, before the page itself ever
renders. One thing worth double-checking here: Middleware only runs for requests that
match its matcher config (or all requests, if none is set), so a query param read that
silently never fires is often a matcher scoping issue rather than a searchParams bug —
exactly the class of problem covered in
Next.js Middleware Not Running on Vercel: 3 Causes.
Further reading: Next.js Redirect from / to another page, Fix: NextRouter was not mounted. Also see Fix: useRouter query undefined on first render. Also see Fix Invariant: attempted to hard navigate to the same URL. Also see Fix Error: NEXT_REDIRECT logged in a Route Handler catch.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix: useRouter query undefined on first render
Why Next.js hydrates a dynamic Pages Router page with an empty query object, how router.isReady fixes it, and why the App Router does not have this exact problem.
Next.js Redirect from / to another page
If your redirect works only after render, flickers, or fires in the wrong place, you are probably using the wrong redirect API for the job. Here is the exact mapping.
Fix Error: NEXT_REDIRECT logged in a Route Handler catch
Reproduced on Next.js 16.4.2: why redirect() inside try/catch logs Error: NEXT_REDIRECT, sends users to the wrong URL or returns a 500, and the three fixes that hold.


