Home Handbooks Next.js 16+ Hooks Technology handbook
Next.js 16+ Hooks — App Router Navigation Every Next.js 16 App Router hook, grouped by the job it performs: reading the URL, navigating, understanding layouts, measuring users and handling connectivity.
Topics 41
Sections 8 Contents URL hooks usePathname Read the current URL pathname from a Client Component.
The current URL's pathname is returned to a Client Component. Reading the URL from a Server Component is unsupported, so layout state survives navigation. The component is downloaded once and re-renders from state rather than being refetched. Example: components/nav-link.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
const pathname = usePathname();
const active = pathname === href || pathname.startsWith(`${href}/`);
return <Link href={href} aria-current={active ? "page" : undefined} className={active ? "active" : ""}>{children}</Link>;
}useSearchParams Read the query string through a read-only URLSearchParams interface.
A read-only URLSearchParams over the current query string is returned. Read-only means an update is a navigation, not a mutation of the object. On a statically prerendered route it needs a Suspense boundary to avoid opting the route out. Example: components/sort-select.tsx
"use client";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
export function SortSelect() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
function onChange(sort: string) {
const params = new URLSearchParams(searchParams);
params.set("sort", sort);
router.replace(`${pathname}?${params}`);
}
return (
<select value={searchParams.get("sort") ?? "newest"} onChange={e => onChange(e.target.value)}>
<option value="newest">Newest</option>
<option value="price">Price</option>
</select>
);
}useParams Read dynamic route parameters from the active URL segment.
Dynamic params filled in by the current URL are returned as an object. It can be given a type parameter, so the returned record is typed rather than cast. A catch-all segment yields an array where a single dynamic segment yields a string. Example: components/review-form.tsx
"use client";
import { useParams } from "next/navigation";
export function ReviewForm() {
const { id } = useParams<{ id: string }>();
return (
<form action={`/api/products/${id}/reviews`} method="post">
<textarea name="text" />
<button>Post review</button>
</form>
);
}URL hooks and Suspense Isolate useSearchParams behind Suspense when a prerendered route reads query state.
useSearchParams is wrapped in Suspense on a statically prerendered route. Without a boundary the whole route opts out of static rendering. The boundary confines the dynamic part to the component that actually reads the query. usePathname with rewrites Account for rewritten paths when displaying pathname-dependent UI and avoid hydration surprises.
A rewrite in next.config or the Proxy file can make the pathname mismatch. The initial value comes from the server and may differ from the browser's pathname. The docs name this as a hydration mismatch and give an example that mitigates it. useSearchParams values Read repeated, optional and encoded query values without mutating the read-only object.
get reads one value; repeated keys need getAll.Values arrive already decoded, so decoding again corrupts them. The object is read-only, so a change means building a new query string. useParams with catch-all routes Handle string and string-array parameters from dynamic, catch-all and optional catch-all segments.
A catch-all segment returns an array of the matched segments. An optional catch-all is absent entirely at the bare parent path. Both shapes must be handled, because the type depends on the matched route. Navigation hooks useRouter Navigate, replace history, refresh data, prefetch routes and go back from client logic.
push, replace, refresh, prefetch, back, forward and bfcacheId are what the hook returns. The docs recommend <Link> unless there is a specific reason to navigate programmatically. It comes from next/navigation; the Pages Router's next/router is a different hook. Example: components/new-project-form.tsx
"use client";
import { useRouter } from "next/navigation";
import { createProject } from "@/app/projects/actions";
export function NewProjectForm() {
const router = useRouter();
async function onSubmit(formData: FormData) {
const project = await createProject(formData);
router.push(`/projects/${project.id}`);
}
return <form action={onSubmit}><input name="name" /><button>Create</button></form>;
}router.push Navigate to a new URL while adding a browser history entry.
A client-side navigation adds an entry to the browser's history stack. It takes scroll and transitionTypes, the latter passed to React's addTransitionType. The docs warn that an unsanitized URL here is an XSS hole, because javascript: URLs execute. router.replace Navigate without adding a new browser history entry.
The same navigation happens without adding a history entry. It is the right call when returning to the previous state would be wrong. It accepts the same scroll and transitionTypes options as push. router.refresh Request a fresh server render while preserving client state and browser position.
The route is re-requested and Server Components re-render. React state and scroll position survive, because the payload is merged rather than remounted. It clears the Client Cache only; server-side caching needs revalidatePath or revalidateTag. router.prefetch Warm a route before a logic-driven navigation occurs.
A route is warmed for a faster client-side transition. An optional onInvalidate callback fires when the prefetched data goes stale. <Link> already prefetches on viewport entry, so this is for navigations without one.useLinkStatus Show pending feedback while a Link navigation is being prepared or completed.
It reports the pending state of the <Link> it is rendered inside. It is imported from next/link, not from next/navigation. The docs prefer loading.js and prefetching, reserving this for dynamic routes without one. Example: components/nav-item.tsx
"use client";
import Link, { useLinkStatus } from "next/link";
function Pending() {
const { pending } = useLinkStatus();
return <span aria-hidden className={pending ? "spinner" : "spinner hidden"} />;
}
export function NavItem({ href, label }: { href: string; label: string }) {
return <Link href={href}>{label} <Pending /></Link>;
}useRouter back and forward Move through browser history without rebuilding or trusting an arbitrary URL.
back() and forward() move through the browser's history stack.Traversing history is safer than guessing a URL to navigate to. There may be no entry to return to, so a fallback destination is needed. useRouter hard navigation safety Prefer trusted internal paths and avoid passing unsanitized user input to navigation methods.
The docs state untrusted URLs must not be passed to push or replace. A javascript: URL passed there executes in the context of your page. Validating against known internal routes is what makes the destination safe. useLinkStatus pending UI Add a small pending indicator inside a Link without replacing the shared navigation shell.
A small element inside the <Link> reflects the pending state. The documented case is a shimmer over the clicked link while navigation completes. It only helps when prefetching is off or the route is dynamic without loading.js. Layout hooks useSelectedLayoutSegment Read the active child segment one level below the layout containing the hook.
The active segment one level below the calling layout is returned. It returns null when there is no segment below, as at the root layout. For a catch-all route the matched segments come back joined, such as 'a/b/c'. Example: app/settings/tabs.tsx
"use client";
import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";
const tabs = [{ slug: "profile", label: "Profile" }, { slug: "billing", label: "Billing" }];
export function SettingsTabs() {
const segment = useSelectedLayoutSegment(); // 'billing' on /settings/billing
return (
<nav role="tablist">
{tabs.map(t => <Link key={t.slug} role="tab" aria-selected={segment === t.slug} href={`/settings/${t.slug}`}>{t.label}</Link>)}
</nav>
);
}useSelectedLayoutSegments Read all active child segments below a layout for breadcrumbs and section navigation.
Every active segment below the calling layout is returned as an array. It is the hook for breadcrumbs, where one level down is not enough. Route groups appear in the array even though they contribute no URL segment. Example: app/docs/breadcrumbs.tsx
"use client";
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
export function Breadcrumbs() {
const segments = useSelectedLayoutSegments().filter(s => !s.startsWith("("));
return (
<nav aria-label="Breadcrumb">
<Link href="/docs">Docs</Link>
{segments.map((s, i) => <span key={s}> › <Link href={`/docs/${segments.slice(0, i + 1).join("/")}`}>{s}</Link></span>)}
</nav>
);
}Segment-aware navigation Combine selected layout segments with route groups and parallel layouts to mark active navigation.
Selected segments drive active state instead of comparing full pathnames. useSelectedLayoutSegment optionally takes a parallelRouteKey to read inside a slot. Segment-based marking survives route groups, which contribute nothing to the path. useSelectedLayoutSegment in tabs Mark a tab active from the child segment directly below its layout.
A tab is active when the segment below its layout matches its own. The segment is the tab's identity, so no path parsing is needed. At the layout's own URL the hook returns null, which is the no-tab state. useSelectedLayoutSegments and route groups Filter invisible route-group segments when building breadcrumbs or section labels.
Route-group segments are filtered out when building breadcrumbs. A group adds no URL segment, so showing it invents a level. Filtering has to be explicit, because the array still contains them. Observability hooks useReportWebVitals Receive Core Web Vitals such as LCP, CLS, INP, TTFB and FCP in a client entry point.
A callback receives each metric as it is measured. The metric carries id, name, delta, value and rating, not just a number. id is unique per page load, which is what lets distributions be built from it.Example: app/web-vitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals(metric => {
navigator.sendBeacon("/api/vitals", JSON.stringify({ name: metric.name, value: metric.value, page: location.pathname }));
});
return null;
}useReportWebVitals analytics Send metric name, value, rating and navigation context to an analytics endpoint.
Metric fields are forwarded to an analytics endpoint. delta is what analytics tools expect for cumulative metrics such as CLS.CLS must be scaled to an integer for tools that reject fractional values. Connectivity hooks useOffline Read whether the browser is currently offline when the experimental capability is enabled.
A boolean reports whether the application is currently offline. It always returns false unless experimental.useOffline is enabled in the config. That flag also turns on automatic retry of blocked navigation, prefetch and Server Action requests. Example: next.config.ts
import type { NextConfig } from "next";
export default { experimental: { useOffline: true } } satisfies NextConfig;useOffline and hydration Treat the initial offline value as progressive enhancement because server and client values begin conservatively.
false means online, rendering on the server, or not yet hydrated.That shared initial value is what keeps the server and client renders agreeing. true follows a failed network request or the browser's own offline event.useOffline fallback UI Offer retry, cached content and disabled-submit feedback when the browser loses connectivity.
Connectivity-aware UI renders from the boolean, such as a banner or a Suspense fallback. Requests blocked while offline are retried automatically once the flag is on. Disabling a submit is what stops a write that would fail anyway. Rules and boundaries Hooks in Client Components Keep Next.js navigation and browser hooks below a use client boundary.
These hooks require a Client Component and the use client directive. The docs are explicit that a Client Component is not a de-optimization. Placing the boundary high pulls the whole subtree into the client bundle. Hooks and Server Components Use server props and route APIs on the server instead of calling client-only hooks there.
A Server Component receives params and searchParams as props. Reading the current URL from the server is deliberately unsupported. That restriction is what allows layout state to persist across navigations. Next.js hook migration from Pages Router Replace next/router APIs with next/navigation hooks when moving screens into the App Router.
next/router is replaced by the hooks in next/navigation.pathname, query and params are now three separate hooks rather than one object.In a project with both directories, usePathname's return type is adjusted automatically. Hook composition in layouts Keep client hook consumers small while passing server-fetched data through stable layout boundaries.
A small Client Component reads the hook and the layout stays a Server Component. Confining the client boundary is the documented, most performant arrangement. A hook called in the layout itself makes everything beneath it client-rendered. Mutations Hooks with Server Functions Pair navigation hooks with Server Functions for post-save redirects and refreshes.
A Server Function mutates and the router moves the user afterwards. redirect called inside the function removes the need to navigate on the client.router.refresh() is what re-reads server data after a mutation.Hook patterns useServerInsertedHTML Inject a CSS-in-JS library's collected styles into the server-rendered HTML from a client provider.
A client provider injects collected styles into the server-rendered HTML. It is how a CSS-in-JS library avoids a flash of unstyled content. The callback can run more than once, so it must not accumulate state. useRouter in event handlers Call the Hook at the top level and navigate from the handler, never the other way round.
The hook is called at the top level and the handler calls the returned methods. Hooks cannot be called conditionally, and a handler is conditional. The router object is stable, so it is safe to use inside a callback. Active links from usePathname Mark the current link in a shared layout without making the whole layout request-dependent.
A small client component compares the pathname to mark the current link. Keeping it small stops the shared layout becoming request-dependent. Under rewrites the comparison can mismatch, which is the documented hazard. Typed useParams Read dynamic segments through the generated route types instead of casting the returned record.
useParams<{ tag: string; item: string }>() types the returned record.Typing at the call site removes a cast that could outlive the route. Generated route types keep the shape honest when a segment is renamed. Search params are read-only The object useSearchParams returns cannot be mutated: build a new query string and navigate.
The returned URLSearchParams cannot be mutated. A change is expressed by building a new query string and navigating. Mutating the object silently does nothing to the URL. Updating the query without a reload Pair a rebuilt query string with router.replace to keep scroll position and client state.
A rebuilt query string is passed to router.replace. replace avoids stacking a history entry for every keystroke. The scroll option prevents the navigation resetting scroll position. Segment hooks at the root Know what useSelectedLayoutSegment returns above the first matched segment, and plan the fallback.
At app/layout.js on /, useSelectedLayoutSegment returns null. Null is a real state to render, not an error to guard against. A layout at its own URL also returns null, which tab code must handle. useReportWebVitals placement Mount it once in a small client component near the root, not in every page that wants metrics.
A dedicated client component near the root mounts it once. The docs call a separate component the most performant approach. Mounting it per page multiplies every metric by the number of listeners. Pending UI thresholds Reveal useLinkStatus pending state after a short delay so a fast navigation does not flash.
The pending indicator is revealed after a short delay. An indicator that flashes and vanishes reads as a glitch rather than progress. The docs frame this as subtle, inline feedback rather than a route-level fallback. Hooks versus server props A Server Component awaits params and searchParams as props; there is no hook to reach for.
A Server Component awaits params and searchParams as props. Both are promises in Next.js 16 and must be awaited. There is no client hook to reach for, because there is no browser to read. ← Next.js 16+ React 19+ Hooks →