How to Access and Manage React Router Query Params in Next.js Components
The most efficient way to handle react router query params in Next.js is using the useRouter hook (Pages Router) or useSearchParams hook (App Router), which provide immutable, parsed query objects that update efficiently via shallow navigation without triggering full page reloads.
Next.js provides a built-in routing solution for React applications that eliminates the need for external routing libraries. Whether you're using the legacy Pages Router or the modern App Router, accessing and managing react router query params follows a consistent pattern optimized for performance. This guide examines the implementation details from the vercel/next.js source code to show you the most efficient patterns for reading and updating URL parameters.
Understanding Next.js Router Architecture
Next.js maintains two distinct routing systems. The Pages Router (next/router) uses a singleton pattern where useRouter returns a router instance with a parsed query object. The App Router (next/navigation) uses React Server Components by default and exposes useSearchParams for client components, returning a URLSearchParams instance.
Both systems prioritize immutability and shallow comparison to minimize re-renders. According to the source code in packages/next/src/client/router.ts, the router creates a shallow copy of the query object via Object.assign in makePublicRouterInstance, ensuring components receive stable references unless the URL actually changes.
Accessing Query Params in the Pages Router
For applications using the pages/ directory, import useRouter from next/router. The hook returns a router object where the query property contains the parsed URL parameters.
import { useRouter } from 'next/router'
export default function ProductList() {
const router = useRouter()
const { query } = router
// Access specific parameters
const page = Number(query.page ?? 1)
const category = query.category as string
return (
<div>
<h1>Category: {category}</h1>
<p>Page: {page}</p>
</div>
)
}
The query object is typed as ParsedUrlQuery and provides an immutable snapshot of the current URL state. As implemented in packages/next/src/client/router.ts lines 69-82, the makePublicRouterInstance function shallow-copies the query to prevent accidental mutations of the internal router state.
Accessing Query Params in the App Router
For the app/ directory, use useSearchParams from next/navigation. This hook returns a URLSearchParams instance that provides a Map-like interface for reading query strings.
'use client'
import { useSearchParams } from 'next/navigation'
export default function Dashboard() {
const searchParams = useSearchParams()
// Read individual parameters
const tab = searchParams.get('tab') ?? 'overview'
const view = searchParams.get('view') ?? 'grid'
return (
<div>
<h1>Current Tab: {tab}</h1>
<p>View Mode: {view}</p>
</div>
)
}
According to packages/next/src/client/components/navigation.ts lines 146-150, useSearchParams wraps the native URLSearchParams constructor to ensure compatibility with React's rendering cycle while maintaining the standard Web API interface.
Efficient Update Patterns with Shallow Navigation
Updating react router query params without triggering a full page reload requires shallow navigation. Both routers support this pattern, though the implementation differs slightly.
Pages Router Updates
Use router.replace with the shallow: true option to update the URL and query object without running data fetching methods like getServerSideProps or getStaticProps.
import { useRouter } from 'next/router'
export default function ProductList() {
const router = useRouter()
const { query, pathname } = router
const page = Number(query.page ?? 1)
const goToPage = (nextPage: number) => {
// Preserve existing query params while updating specific values
router.replace(
{
pathname,
query: { ...query, page: nextPage },
},
undefined,
{ shallow: true } // Critical: prevents data refetch
)
}
return (
<div>
<p>Current page: {page}</p>
<button onClick={() => goToPage(page - 1)} disabled={page <= 1}>
Prev
</button>
<button onClick={() => goToPage(page + 1)}>Next</button>
</div>
)
}
As shown in packages/next/src/client/router.ts, shallow navigation skips the data fetching phase while still emitting the route change event and updating the query snapshot.
App Router Updates
In the App Router, shallow navigation is the default behavior for client-side transitions. Use router.replace from next/navigation to update search parameters.
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
export default function Dashboard() {
const searchParams = useSearchParams()
const router = useRouter()
const tab = searchParams.get('tab') ?? 'overview'
const setTab = (newTab: string) => {
// Clone to avoid mutating the original read-only instance
const params = new URLSearchParams(searchParams)
params.set('tab', newTab)
// Shallow replace updates URL without server round-trip
router.replace(`?${params.toString()}`, { shallow: true })
}
return (
<>
<nav>
<button onClick={() => setTab('overview')}>Overview</button>
<button onClick={() => setTab('stats')}>Stats</button>
</nav>
<section>{tab === 'overview' ? <Overview /> : <Stats />}</section>
</>
)
}
Implementation Details from Source Code
The efficiency of Next.js query parameter handling stems from architectural decisions visible in the vercel/next.js repository.
Singleton Router Pattern: In packages/next/src/client/router.ts, the router is created as a singleton via createRouter. This ensures that all components calling useRouter receive the same instance, eliminating duplicate URL parsing overhead. The useRouter implementation (lines 38-46) simply reads from RouterContext.
Immutable Query Snapshots: The makePublicRouterInstance function (lines 69-82 in packages/next/src/client/router.ts) uses Object.assign to create a shallow copy of the query object. This guarantees React's reference equality checks work correctly—components only re-render when the URL actually changes, not when the router instance mutates internal state.
Shallow Navigation Optimization: The router's change method distinguishes between shallow and deep navigation. When shallow: true is passed to router.replace, the method skips data fetching hooks (getServerSideProps, etc.) and only updates the URL bar and internal query state, triggering a fast client-side re-render.
Summary
- Use
useRouterfromnext/router(Pages Router) oruseSearchParamsfromnext/navigation(App Router) to read react router query params efficiently. - Query objects are immutable snapshots created via shallow copy in
makePublicRouterInstance, ensuring stable references for React's rendering optimization. - Update parameters with
router.replaceand{ shallow: true }to modify the URL without data refetching, ideal for client-side filtering and pagination. - App Router uses
URLSearchParamsinterface while Pages Router uses a plain object (ParsedUrlQuery), requiring different cloning strategies when updating.
Frequently Asked Questions
What is the difference between useRouter and useSearchParams?
useRouter is the legacy hook for the Pages Router (next/router) that returns a router object containing the query property as a ParsedUrlQuery object. useSearchParams is the App Router (next/navigation) hook that returns a URLSearchParams instance, providing a Map-like API with methods like .get() and .set(). The App Router version is read-only and requires useRouter from the same module to perform navigation updates.
How do I prevent full page reloads when updating query parameters?
Use shallow navigation by passing { shallow: true } as the third argument to router.replace() in the Pages Router, or as an option object in the App Router. This updates the URL bar and the internal router state without triggering data fetching methods like getServerSideProps or server component re-rendering. According to the source code in packages/next/src/client/router.ts, shallow navigation skips the data fetching phase while still emitting the route change event and updating the query snapshot.
Can I directly mutate the query object returned by useRouter?
No, you should never mutate the query object directly. The Next.js source code in packages/next/src/client/router.ts creates an immutable snapshot via Object.assign in the makePublicRouterInstance function (lines 69-82). While direct mutation might not throw an immediate error, it violates React's immutability principles and can cause unpredictable rendering behavior or stale data. Always treat the query object as read-only and use router.replace() with a new query object to update parameters.
Why does the App Router use URLSearchParams instead of a plain object?
The App Router adopts the standard Web API URLSearchParams interface to align with modern browser standards and provide a consistent API for handling search parameters. Unlike the Pages Router's plain object (ParsedUrlQuery), URLSearchParams automatically handles edge cases like duplicate keys and provides convenient methods like .get(), .getAll(), .has(), and .set(). As implemented in packages/next/src/client/components/navigation.ts lines 146-150, this approach ensures type safety and immutability while maintaining compatibility with standard URL manipulation patterns.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →