Client-side Navigation
The <Link> component is your primary tool for client-side transitions. useRouter (from next/navigation) handles programmatic navigation in Client Components.
What You'll Learn
- Prefetching behaviour and how to disable it
router.push(),router.replace(),router.back()- Reading query params with
useSearchParams
Navigation with <Link> and useRouter
In a traditional website, every link click causes a full page reload — the browser throws away everything, requests a new HTML document from the server, and repaints from scratch. Next.js gives you client-side navigation instead: when a user clicks a link, only the changed content swaps out, layouts stay alive, and the transition feels instant.
There are two tools for navigation in the App Router: the <Link> component
for declarative links in JSX, and the useRouter hook for programmatic navigation
from event handlers. This lesson covers both in depth.
1. The <Link> Component
<Link> is imported from next/link and works like a standard
HTML <a> tag — but with client-side navigation built in:
import Link from "next/link";
export default function Navbar() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
Under the hood, Next.js renders this as a regular <a> tag in the HTML —
so it's fully accessible, works without JavaScript, and search engines crawl it normally.
The client-side magic is layered on top.
Linking to Dynamic Routes
You can build dynamic href values with template literals just like any
JavaScript string:
const posts = [
{ slug: "hello-world", title: "Hello World" },
{ slug: "nextjs-tips", title: "Next.js Tips" },
];
export default function PostList() {
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
The href Object Form
For URLs with query strings, you can pass an object instead of a string — this is cleaner and avoids manual string concatenation errors:
// These two are equivalent:
<Link href="/search?query=nextjs&page=2">Search</Link>
<Link href={{ pathname: "/search", query: { query: "nextjs", page: 2 } }}>
Search
</Link>
2. Prefetching — How Next.js Makes Navigation Feel Instant
This is where <Link> really shines. Next.js automatically
prefetches the linked page in the background when a <Link>
enters the user's viewport. By the time they click it, the page data is already loaded —
navigation feels nearly instant.
Prefetching behaviour depends on the render strategy:
- Static routes (no dynamic data): the full page is prefetched and cached when the link scrolls into view.
-
Dynamic routes: Next.js prefetches the shared layouts down to
the first
loading.tsxboundary — enough to show the loading skeleton instantly while the actual data loads.
Disabling Prefetching
Prefetching is on by default in production. You can disable it per-link if needed — for example, on links that are very rarely clicked and would waste bandwidth:
<Link href="/rarely-visited" prefetch={false}>
Rarely Visited Page
</Link>
Note: prefetching only happens in production (npm run build && npm start).
In development (npm run dev) it's disabled — so don't panic if navigation
feels slower locally.
3. Highlighting the Active Link
A very common need is styling the current page's link differently — an underline, a bold
weight, a different colour. <Link> doesn't do this automatically, but
the usePathname hook makes it easy:
"use client"; // usePathname is a hook — needs Client Component
import Link from "next/link";
import { usePathname } from "next/navigation";
const navLinks = [
{ href: "/", label: "Home" },
{ href: "/about", label: "About" },
{ href: "/blog", label: "Blog" },
];
export default function Navbar() {
const pathname = usePathname();
return (
<nav>
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
style={{
fontWeight: pathname === link.href ? "bold" : "normal",
textDecoration: pathname === link.href ? "underline" : "none",
}}
>
{link.label}
</Link>
))}
</nav>
);
}
Because usePathname is a React hook, the component using it must be a
Client Component — add "use client" at the top of the file. This is fine;
a navbar is interactive UI that belongs on the client anyway.
Matching Nested Routes
If you want /blog to also be highlighted when the user is on
/blog/my-post, use startsWith instead of strict equality:
const isActive =
link.href === "/"
? pathname === "/"
: pathname.startsWith(link.href);
4. Controlling Scroll Behaviour
By default, Next.js scrolls to the top of the page on every navigation. You can turn this
off per-link with the scroll prop:
// Don't scroll to top — useful for paginated lists or tab switching
<Link href="/blog?page=2" scroll={false}>
Next Page
</Link>
With scroll={false}, the page content updates but the scroll position stays
exactly where it was — great for infinite scroll implementations or tab-style navigation
where the list is already in view.
5. useRouter — Programmatic Navigation
Sometimes you need to navigate in response to an event — after a form submission, after
a timer, or based on some logic. That's what useRouter is for.
Important: in the App Router, import useRouter from
next/navigation — NOT from next/router. The old
next/router is for the Pages Router and will not work correctly in
app/.
"use client";
import { useRouter } from "next/navigation";
export default function LoginForm() {
const router = useRouter();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// ... handle login logic ...
router.push("/dashboard"); // navigate after successful login
}
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Log In</button>
</form>
);
}
Router Methods
The router object gives you several navigation methods:
| Method | What it does |
|---|---|
router.push(href) |
Navigate to a new route and add it to the history stack. |
router.replace(href) |
Navigate to a new route but replace the current history entry — the back button skips the replaced page. |
router.back() |
Go back one step in the browser history — equivalent to clicking the back button. |
router.forward() |
Go forward one step in the browser history. |
router.refresh() |
Re-fetch server data for the current route without a full page reload. |
router.prefetch(href) |
Manually prefetch a route before the user navigates to it. |
push vs replace — When Does It Matter?
The difference between push and replace is about the browser
history stack:
-
push: user goes A → B → C. Pressing back goes C → B → A. Use this for normal navigation. -
replace: user goes A → B → C (replace). Pressing back goes C → A. B is gone from history. Use this for redirects after login/logout, or when you don't want the user to be able to "go back" to a transient state like a loading page or a completed form step.
// After logout — replace so they can't hit back to the dashboard
router.replace("/login");
// After completing step 1 of a multi-step form — replace so back doesn't revisit it
router.replace("/onboarding/step-2");
6. Reading the Current URL — usePathname & useSearchParams
Beyond navigation, you often need to read the current URL — to highlight active links, filter content, or restore state from the URL. The App Router provides two hooks for this (both require Client Components):
usePathname
Returns the current URL path as a string, without the query string:
"use client";
import { usePathname } from "next/navigation";
export default function Breadcrumb() {
const pathname = usePathname();
// e.g. "/dashboard/settings"
const segments = pathname.split("/").filter(Boolean);
// ["dashboard", "settings"]
return (
<nav aria-label="breadcrumb">
{segments.map((segment, i) => (
<span key={i}>
{i > 0 && " / "}
{segment}
</span>
))}
</nav>
);
}
useSearchParams
Returns the current URL's query string as a URLSearchParams object:
"use client";
import { useSearchParams } from "next/navigation";
export default function SearchResults() {
const searchParams = useSearchParams();
const query = searchParams.get("query"); // e.g. "nextjs"
const page = searchParams.get("page"); // e.g. "2"
return (
<div>
<p>Showing results for: {query}</p>
<p>Page: {page ?? "1"}</p>
</div>
);
}
Updating Search Params Without useRouter
For updating query params — filters, sort order, pagination — it's often cleaner to
build the new URL and use router.push or router.replace:
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
export default function SortControl() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
function handleSortChange(sort: string) {
const params = new URLSearchParams(searchParams.toString());
params.set("sort", sort);
router.replace(`${pathname}?${params.toString()}`);
}
return (
<select onChange={(e) => handleSortChange(e.target.value)}>
<option value="newest">Newest</option>
<option value="popular">Most Popular</option>
<option value="oldest">Oldest</option>
</select>
);
}
Using replace here means each sort change doesn't add a new history entry —
the user can press back to leave the page entirely rather than cycling through every
sort option they tried.
7. Server-side Redirects
useRouter only works in Client Components. In Server Components, layouts,
and Server Actions, use the redirect() function from next/navigation
instead:
// app/dashboard/page.tsx (Server Component)
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/auth";
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) {
redirect("/login"); // Server-side redirect — no JS needed
}
return <h1>Welcome, {user.name}</h1>;
}
redirect() throws internally (it's implemented as a thrown error that Next.js
catches), so you don't need a return after it — but it's good practice to
write code as if you do, to keep TypeScript happy.
For permanent redirects (301), use permanentRedirect() from the same import.
Use this when a URL has moved forever — it tells search engines to update their index.
import { permanentRedirect } from "next/navigation";
permanentRedirect("/new-url"); // 308 permanent redirect
8. Common Gotchas
-
Importing from the wrong package.
useRouter,usePathname, anduseSearchParamsall come fromnext/navigationin the App Router. Usingnext/router(the Pages Router package) will either error or behave incorrectly. -
Using
useRouterin a Server Component. It's a hook — it only works in Client Components. For server-side redirects useredirect(). -
Expecting prefetch to work in development. Prefetching is a production
optimization only. Test it with
npm run build && npm start. -
router.refresh()is not a page reload. It re-fetches server data and re-renders Server Components for the current route, but Client Component state (like form inputs) is preserved. This is useful after a Server Action mutates data. -
useSearchParamsmust be wrapped in Suspense when used in a component that isn't already inside a Suspense boundary — otherwise Next.js will warn about missing suspense during static rendering.
Key Takeaways
<Link href="...">is your primary navigation tool — use it for all in-app links in JSX.- Next.js automatically prefetches linked pages when they enter the viewport in production, making navigation feel instant.
- Use
usePathname()to read the current path and highlight active links. useRouter()(fromnext/navigation) handles programmatic navigation —pushadds to history,replaceoverwrites it.useSearchParams()reads query parameters from the URL.- In Server Components and Server Actions, use
redirect()orpermanentRedirect()instead ofuseRouter.
Next up: Lesson 105 — Loading UI & Suspense Boundaries. You'll learn how to add instant loading skeletons to any route with a single file, and how to use React Suspense to stream slow content progressively to the browser.