ProgrUmar Logo
Module 1: Foundations & Routing

Navigation with <Link> and useRouter

Duration: 10 mins

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.tsx boundary — 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, and useSearchParams all come from next/navigation in the App Router. Using next/router (the Pages Router package) will either error or behave incorrectly.
  • Using useRouter in a Server Component. It's a hook — it only works in Client Components. For server-side redirects use redirect().
  • 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.
  • useSearchParams must 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() (from next/navigation) handles programmatic navigation — push adds to history, replace overwrites it.
  • useSearchParams() reads query parameters from the URL.
  • In Server Components and Server Actions, use redirect() or permanentRedirect() instead of useRouter.

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.

Chat with us