
Next.js usePathname
Every app eventually needs a component that behaves differently depending on where the user currently is — a nav link that highlights itself when active, an analytics hook that fires on every route change, a breadcrumb trail that reads itself off the URL. In the Pages Router you'd reach for router.pathname. In the App Router, that job belongs to usePathname, a small, single-purpose hook that hands a Client Component the current URL's pathname as a plain string.
It looks trivial on the surface — one function call, one string back — but the constraints around where and how you're allowed to use it say a lot about how the App Router thinks about state, prerendering, and the boundary between server and client. This article works through the full API surface, the newer Cache Components behavior it interacts with, and the hydration pitfalls that trip people up when usePathname meets rewrites or a Proxy.
Why usePathname Only Works in Client Components
usePathname is deliberately gated behind the 'use client' boundary. You cannot call it in a Server Component, and there's no server-side equivalent that reads the pathname the same way. This isn't an oversight — it's intentional, and understanding why clarifies a lot about how routing state works in the App Router.
Server Components render once per request (or once at build time, if the route is static) and then get discarded — the server doesn't hold onto any notion of "the current page" between renders. Client Components, by contrast, persist in the browser across navigations. When you navigate from /dashboard to /dashboard/settings, the App Router doesn't tear down and remount every component in the tree; it re-renders only what changed and reuses the rest. A Client Component that calls usePathname participates in that persistence: it was downloaded once as part of the client JavaScript bundle, and it simply re-renders with a new string every time the route changes, rather than being re-fetched from the server.
This matters for a specific, easy-to-miss reason: layout state preservation. If reading the pathname were possible from a Server Component, doing so inside a layout.tsx would force that layout to re-render (and potentially remount) on every navigation just to pick up the new URL — defeating the entire point of layouts persisting across route changes. Keeping usePathname client-only means your layouts stay mounted, and only the specific Client Component that actually needs the pathname re-renders when it changes.
If you're coming from the Pages Router, usePathname is one of a small family of hooks (useRouter, useSearchParams, useParams) that fill in for what used to be a single unified router object.
Basic Usage
// app/example-client-component.tsx
"use client";
import { usePathname } from "next/navigation";
export default function ExampleClientComponent() {
const pathname = usePathname();
return <p>Current pathname: {pathname}</p>;
}
Note the import path: next/navigation, not next/router. This is one of the most common mistakes when migrating from the Pages Router — next/router's useRouter().pathname and next/navigation's usePathname() look similar but come from entirely different modules, and mixing them up produces a confusing "not a function" error rather than a helpful one.
Parameters
usePathname takes no parameters. There's no way to configure it, no options object, nothing to pass in. It reads the current route and returns a value — that's the entire contract.
Return Value
usePathname returns a plain string representing the current URL's pathname. Query parameters are stripped out — that's useSearchParams's job, not this one.
| URL | Returned value |
|---|---|
/ | '/' |
/dashboard | '/dashboard' |
/dashboard?v=2 | '/dashboard' |
/blog/hello-world | '/blog/hello-world' |
That query-stripping behavior is worth internalizing early, because it's a frequent source of confusion: if you're trying to detect whether the user is on a specific view of a page (say, /dashboard?tab=billing vs. /dashboard?tab=usage), usePathname alone won't tell you — both will return '/dashboard'. You'll need useSearchParams alongside it for that distinction.
Behavior Under Cache Components
This is the part of the docs that's genuinely new territory if you learned the App Router before cacheComponents existed, and it's worth spending real time on, because it changes what "just add usePathname" actually costs you.
When cacheComponents is enabled in next.config, whether usePathname can resolve during prerendering depends entirely on whether the pathname is knowable at build time:
Static routes, and dynamic routes fully covered by generateStaticParams — every segment of the URL, including any dynamic params, is known ahead of time. In this case usePathname resolves during prerendering with no extra ceremony. No Suspense boundary required.
Dynamic routes with params not covered by generateStaticParams — these are "fallback" segments whose value isn't known until an actual request comes in. Here, usePathname can't be resolved during the static prerender pass, so the component that calls it suspends. If you haven't wrapped it (or a parent) in a Suspense boundary, the build fails outright rather than silently degrading — which is Next.js being deliberately strict about surfacing this rather than letting you ship a subtly broken page.
The part that catches people off guard: this suspension propagates upward through static components too. Picture a sidebar component in a shared layout that calls usePathname to highlight the active nav link. That sidebar itself has no dynamic data — it's rendering the same nav links regardless of route. But if it's rendered above even one page in the route tree that has an unresolved dynamic param, the sidebar suspends right along with it, because React's Suspense boundaries operate on the render tree, not on "does this specific component need dynamic data."
The fix is the same pattern used throughout Cache Components: wrap the pathname-reading component (or a parent of it) in its own Suspense boundary, so it gets its own fallback and doesn't drag the rest of a static layout into a suspended state:
// app/layout.tsx
import { Suspense } from "react";
import { NavSidebar } from "./nav-sidebar";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="layout">
<Suspense fallback={<div className="sidebar-skeleton" />}>
<NavSidebar />
</Suspense>
<main>{children}</main>
</div>
);
}
If you hit a build error pointing at "URL data in a Client Component outside of Suspense," this is almost always the underlying cause — a usePathname (or useSearchParams, or useParams) call reachable from a route whose params aren't fully enumerated at build time.
Responding to Route Changes
A common pattern is running a side effect — analytics tracking, closing a mobile menu, scrolling to top — whenever the route changes. usePathname combined with useSearchParams inside a useEffect covers this:
// app/analytics-tracker.tsx
"use client";
import { useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
export function AnalyticsTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const url = searchParams.toString()
? `${pathname}?${searchParams.toString()}`
: pathname;
// send `url` to your analytics provider
}, [pathname, searchParams]);
return null;
}
Both hooks need to be in the dependency array — pathname alone won't catch a change from /dashboard?tab=billing to /dashboard?tab=usage, since the pathname itself doesn't change in that case.
Avoiding Hydration Mismatches With Rewrites and Proxy
This is the single most likely way usePathname will bite you in production, and it's genuinely subtle. When a page is prerendered, the static HTML reflects the source pathname the page was built from. If a user actually reaches that page through a rewrite (configured in next.config, or via a proxy.ts/proxy.js file), the URL the browser shows can differ from the source path the server rendered.
Concretely: say you have a rewrite that maps /blog/:slug to /posts/[slug]. The page component lives under app/posts/[slug], so the prerendered HTML for a given post reflects /posts/some-post. But the visitor's browser address bar shows /blog/some-post, because that's the URL that was actually requested. usePathname() on the client reads the actual browser URL — /blog/some-post — while the server-rendered HTML baked in /posts/some-post. React notices the mismatch during hydration and throws a hydration error, because the client and server disagree about what got rendered.
The fix isn't to avoid usePathname in rewritten routes — it's to design the affected UI so only a small, isolated slice of it actually depends on the client-read pathname, and to defer reading that value until after mount:
// app/pathname-badge.tsx
"use client";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
export default function PathnameBadge() {
const pathname = usePathname();
const [clientPathname, setClientPathname] = useState("");
useEffect(() => {
setClientPathname(pathname);
}, [pathname]);
return (
<p>
Current pathname: <span>{clientPathname}</span>
</p>
);
}
On the server, clientPathname renders as an empty string — no mismatch, because the server never claimed to know the "real" pathname. After mount, the useEffect picks up the actual browser pathname and updates the state, so the visible value corrects itself a moment after hydration. That correction is technically visible for a frame or two, which is exactly the kind of flash covered in Preventing flash before hydration — if the flicker matters for your UI (a highlighted nav item snapping into place, say), the techniques there (inline scripts, suppressHydrationWarning, CSS-only initial states) apply here too.
The practical rule of thumb: if your project has rewrites or a Proxy file that can change the visible URL relative to the route a page actually lives at, don't build large swaths of UI directly around usePathname()'s return value. Isolate the dependency to the smallest component you can.
Pages Router Compatibility
If your project has both an app and a pages directory — common during an incremental migration — and a component using usePathname gets imported into a pages-directory route, be aware that usePathname can return null in that context. This happens in situations like fallback routes (getStaticPaths with fallback: true) or during Automatic Static Optimization, where the Pages Router hasn't finished initializing its router state yet.
Next.js automatically widens the return type of usePathname to string | null when it detects both directories coexisting in a project, specifically to nudge you toward handling this case rather than assuming a string is always guaranteed. If you're mid-migration and sharing components between the two routers, guard against null explicitly rather than assuming usePathname() always returns a string.
Common Mistakes
Importing from next/router instead of next/navigation. This is the single most common error, especially for anyone with Pages Router muscle memory. next/router's hooks don't work in the App Router at all.
Expecting usePathname to include query params. It never does — /dashboard?tab=billing and /dashboard?tab=usage both return '/dashboard'. Pull in useSearchParams if the query string matters to your logic.
Calling it from a Server Component. There's no server-side version of this hook. If you need the pathname on the server — for generateMetadata, for instance — you're generally working from params and searchParams passed into the page, not from a pathname-reading hook.
Building large static-looking UI directly on top of a usePathname() value in a project with rewrites. As covered above, this is the classic setup for a hydration mismatch that only shows up in specific navigation paths, making it maddening to reproduce locally if you're testing by hitting routes directly instead of through the rewritten URL.
Forgetting the Suspense boundary under Cache Components. If a build fails referencing "URL data in a Client Component outside of Suspense," look for a usePathname call (or useSearchParams/useParams) somewhere in a component tree shared with a route that has unresolved dynamic params.
usePathname vs. the Alternatives
It's easy to reach for usePathname reflexively when what you actually want is a related-but-different hook:
| You want to know... | Use |
|---|---|
| The current URL's path, without query params | usePathname |
| The current URL's query string | useSearchParams |
| The dynamic route params for the current segment | useParams |
| Which parallel-route slot is active | useSelectedLayoutSegment / useSelectedLayoutSegments |
| Programmatic navigation (push/replace/refresh) | useRouter |
Key Takeaways
| Aspect | Behavior |
|---|---|
| Availability | Client Components only — no Server Component equivalent |
| Parameters | None |
| Return value | A string (query params stripped); string | null in mixed Pages/App projects |
| Cache Components | Resolves at build time for fully static/enumerated routes; suspends and requires a Suspense boundary for fallback dynamic params |
| Rewrites/Proxy | Client-read pathname can diverge from the server-rendered source path — isolate the affected UI and read after mount |
| Common pairing | useSearchParams (for query state), useEffect (for route-change side effects) |
usePathname is one of the smallest hooks in the App Router's API surface, but the constraints wrapped around it — client-only, no query params, Suspense-aware under Cache Components, hydration-sensitive under rewrites — are a fairly complete tour of how seriously the framework takes the split between what the server can know ahead of time and what only the browser can tell you for certain.


