Four hooks from next/navigation tell a Client Component where it is and move it somewhere else: useRouter() for navigating from an event handler, usePathname() for the path without its query string, useSearchParams() for a read-only URLSearchParams, and useSelectedLayoutSegment() for the child segment a layout is showing, which is how tab bars highlight themselves. All four are client-only; a Server Component reads the URL through its params and searchParams props instead.
router.push(href, options) and router.replace(href, options) do what a <Link> click does, minus the prefetch, so use them only where there is no link to click: after a login, on a timer, from a <select>. refresh() re-requests the current route and merges the new payload into the live tree, keeping useState and scroll position. A sort control in the demo writes its state into the query string:
'use client'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
export default function Filter() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
function setSort(next) {
const params = new URLSearchParams(searchParams)
params.set('sort', next)
router.replace(`${pathname}?${params}`, { scroll: false })
}
return <p id="sort">sorted by {searchParams.get('sort') ?? 'new'}</p>
}Clicking the Price button changed the URL and the rendered text without reloading the document, but it did cost a round trip, because replace goes through the router:
before: sorted by new http://localhost:3311/search after : sorted by price http://localhost:3311/search?sort=price requests during the click: ['/search?sort=price&_rsc=nBL9Aow_LoP9jNQq']
When no Server Component depends on the query string, that request is waste. Next.js 10,514 wires the native History API into its router, so window.history.replaceState(null, '', '?sort=price') updates the address bar, keeps useSearchParams in sync and contacts no server. Use router.replace when the server must re-render, replaceState when only client state changed.
Two traps catch everyone once. useSearchParams forces client-side rendering up to the nearest <Suspense> boundary during prerendering, because a query string does not exist at build time — that is why Filter is wrapped in <Suspense> inside app/search/page.js, and why forgetting the wrapper fails the build. And usePathname in a small Client Component is the only way to react to navigation from a layout, since layouts do not re-render (layout.js and the Root Layout); comparing it with each href gives the active-link pattern, aria-current={pathname === href ? 'page' : undefined}.