Navigation in Next.js 🧭

Introduction 👋

Navigation is how users move between pages in a Next.js application. The App Router provides a rich navigation toolkit — from the simple <Link> component to a full suite of hooks for reading and reacting to the URL. This tutorial covers everything you need to build fast, accessible, and dynamic navigation experiences.

Information

All navigation APIs discussed here are part of next/navigation and next/link, designed specifically for the App Router.

What is Navigation? 🧠

In a Next.js app, navigation refers to moving between routes, either by clicking a link, calling a router function, or triggering a redirect on the server. Next.js optimizes navigation by prefetching routes and reusing shared layouts, avoiding unnecessary full-page reloads.

  • Client-side navigation — instant transitions handled by JavaScript in the browser.
  • Server-side navigation — full page loads or server-issued redirects.

Client-Side Navigation đŸ’ģ

Client-side navigation happens entirely in the browser using the App Router's client-side JavaScript, without a full page reload. Only the changed segments re-render, while shared layouts persist their state.

components/Nav.tsx

import Link from "next/link";

export default function Nav() {
  return <Link href="/dashboard">Dashboard</Link>;
}

Tip

Client-side navigation is what makes Next.js apps feel like single-page applications, even though pages are server-rendered.

Server-Side Navigation đŸ–Ĩī¸

Server-side navigation occurs when the browser makes a fresh request to the server — for example, a hard refresh, typing a URL directly, or a server-issued redirect() call. This always results in a full page load.

app/dashboard/page.tsx

import { redirect } from "next/navigation";

export default function Dashboard() {
  const isAuthenticated = false;
  if (!isAuthenticated) {
    redirect("/login");
  }
  return <h1>Dashboard</h1>;
}

<Link> Component 🔗

The <Link> component is the primary way to navigate between routes. It extends the HTML <a> element with automatic prefetching and client-side transitions.

components/Nav.tsx

import Link from "next/link";

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/blog" prefetch={false}>Blog</Link>
    </nav>
  );
}
PropPurpose
hrefdestination path (required)
prefetchenable or disable prefetching
replacereplace history instead of pushing
scrollcontrol automatic scroll-to-top

useRouter 🧭

The useRouter hook, imported from next/navigation, gives Client Components imperative access to the router — useful for navigating in response to events like form submissions or button clicks.

components/LoginForm.tsx

"use client";

import { useRouter } from "next/navigation";

export default function LoginForm() {
  const router = useRouter();

  function handleLogin() {
    router.push("/dashboard");
  }

  return <button onClick={handleLogin}>Log in</button>;
}

Important

useRouter from next/navigation is not the same as the Pages Router's next/router — they are separate APIs.

usePathname đŸ›Ŗī¸

The usePathname hook returns the current URL's pathname as a string, which is commonly used for tasks like highlighting active navigation links.

components/Nav.tsx

"use client";

import { usePathname } from "next/navigation";

export default function Nav() {
  const pathname = usePathname();
  return <p>Current path: {pathname}</p>;
}

useSearchParams 🔎

The useSearchParams hook reads the current URL's query string as a read-only URLSearchParams-like object, and automatically re-renders the component when it changes.

components/SearchResults.tsx

"use client";

import { useSearchParams } from "next/navigation";

export default function SearchResults() {
  const searchParams = useSearchParams();
  const query = searchParams.get("q");
  return <p>Searching for: {query}</p>;
}

Warning

Components using useSearchParams should be wrapped in a <Suspense> boundary, or the whole route may opt into client-side rendering up to that point.

useParams 🧩

The useParams hook returns the dynamic route parameters for the current route as a key-value object, mirroring the folder's dynamic segments.

components/PostHeader.tsx

"use client";

import { useParams } from "next/navigation";

export default function PostHeader() {
  const params = useParams<{ slug: string }>();
  return <h1>Post: {params.slug}</h1>;
}

redirect â†Ēī¸

The redirect function, callable from Server Components, Route Handlers, and Server Actions, immediately redirects the user to a new route with a temporary (307) status by default.

app/profile/page.tsx

import { redirect } from "next/navigation";

export default function Profile({ isLoggedIn }: { isLoggedIn: boolean }) {
  if (!isLoggedIn) {
    redirect("/login");
  }
  return <h1>Profile</h1>;
}

Caution

redirect() throws internally, so it must be called outside of a try/catch block, or the redirect will be swallowed.

permanentRedirect 🔁

permanentRedirect behaves like redirect but issues a permanent (308) redirect, signaling to browsers and search engines that the resource has moved for good.

app/old-page/page.tsx

import { permanentRedirect } from "next/navigation";

export default function OldPage() {
  permanentRedirect("/new-page");
}

notFound đŸšĢ

Calling notFound() inside a Server Component renders the nearest not-found.tsx file and sets the response status to 404.

app/products/[id]/page.tsx

import { notFound } from "next/navigation";

export default async function Product({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  if (!product) {
    notFound();
  }
  return <h1>{product.name}</h1>;
}

Programmatic Navigation ⚡

Programmatic navigation means triggering route changes in response to logic rather than a direct link click — for example, after a successful form submission or an API call.

components/CreatePost.tsx

"use client";

import { useRouter } from "next/navigation";

export default function CreatePost() {
  const router = useRouter();

  async function handleSubmit() {
    const post = await createPost();
    router.push(`/blog/${post.slug}`);
  }

  return <button onClick={handleSubmit}>Publish</button>;
}
MethodBehavior
router.push()navigate and add a new history entry
router.replace()navigate without adding a history entry
router.back()go back one entry in history
router.forward()go forward one entry in history
router.refresh()re-fetch the current route from the server

Dynamic Navigation 🔀

Dynamic navigation builds route paths at runtime, often by interpolating IDs, slugs, or other values into the href — commonly used for lists of items that each link to their own detail page.

components/ProductList.tsx

import Link from "next/link";

export default function ProductList({ products }: { products: { id: string; name: string }[] }) {
  return (
    <List type="unordered">
      {products.map((product) => (
        <Link key={product.id} href={`/products/${product.id}`}>{product.name}</Link>
      ))}
    </List>
  );
}

Route Parameters 🧷

Route parameters come from dynamic segments in the URL path itself — like [id] or [slug] — and are accessible via the params prop in Server Components or the useParams hook in Client Components.

app/products/[id]/page.tsx

export default function Product({ params }: { params: { id: string } }) {
  return <p>Product ID: {params.id}</p>;
}

Query Parameters đŸˇī¸

Query parameters are key-value pairs appended to a URL after a ?, such as ?sort=price&order=asc. They're useful for filters, sorting, and pagination state that should be shareable via URL.

components/SortLink.tsx

import Link from "next/link";

export default function SortLink() {
  return <Link href="/products?sort=price&order=asc">Sort by price</Link>;
}

URL Search Parameters 🔡

On the server, the searchParams prop gives Server Components direct access to the current URL's query string, allowing data fetching to be driven by the URL itself.

app/products/page.tsx

export default function Products({
  searchParams,
}: {
  searchParams: { sort?: string };
}) {
  const sort = searchParams.sort ?? "default";
  return <p>Sorting by: {sort}</p>;
}

Note

Reading searchParams opts a route segment into dynamic rendering, since query strings can't be known at build time.

Navigation Events 📡

Because the App Router doesn't expose traditional lifecycle events, navigation transitions are typically observed by combining usePathname and useSearchParams inside a useEffect to detect route changes.

components/RouteListener.tsx

"use client";

import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";

export default function RouteListener() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    console.log("Route changed:", pathname, searchParams.toString());
  }, [pathname, searchParams]);

  return null;
}

Active Links đŸŽ¯

Building active link indicators involves comparing the current pathname against each link's href and applying conditional styling.

components/NavLink.tsx

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";

export default function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  const pathname = usePathname();
  const isActive = pathname === href;

  return (
    <Link href={href} className={isActive ? "active" : ""}>
      {children}
    </Link>
  );
}

Nested Navigation đŸĒ†

Nested navigation menus — like a sidebar with sub-items — are built by combining nested layouts with <Link> components, letting each level of the UI persist independently as the user drills deeper.

Root Nav
Dashboard
Billing
Overview
Settings

Breadcrumb Navigation 🍞

Breadcrumbs show the user's current location within the site hierarchy and are typically built by splitting usePathname() into segments.

components/Breadcrumbs.tsx

"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";

export default function Breadcrumbs() {
  const pathname = usePathname();
  const segments = pathname.split("/").filter(Boolean);

  return (
    <nav>
      <Link href="/">Home</Link>
      {segments.map((segment, i) => (
        <Link key={segment} href={"/" + segments.slice(0, i + 1).join("/")}>
          {segment}
        </Link>
      ))}
    </nav>
  );
}

Pagination Navigation 📑

Pagination is commonly implemented with query parameters, combining searchParams for the current page and <Link> components for the previous and next pages.

app/blog/page.tsx

import Link from "next/link";

export default function Blog({ searchParams }: { searchParams: { page?: string } }) {
  const page = Number(searchParams.page ?? "1");

  return (
    <div>
      <Link href={`/blog?page=${page - 1}`}>Previous</Link>
      <Link href={`/blog?page=${page + 1}`}>Next</Link>
    </div>
  );
}

Scroll Restoration 📜

By default, Next.js automatically scrolls to the top of a new page on navigation, and restores scroll position on back/forward navigation. This can be disabled per-link using the scroll prop.

components/AnchorLink.tsx

import Link from "next/link";

export default function AnchorLink() {
  return <Link href="/faq#pricing" scroll={false}>Jump to Pricing</Link>;
}

Prefetching ⚡

Prefetching preloads a route's code and data in the background before the user clicks it, making the eventual navigation feel instant. <Link> components prefetch automatically when they enter the viewport.

prefetch valueBehavior
true (default for static routes)full route prefetched
null (default for dynamic routes)only the loading state is prefetched
falseprefetching disabled entirely

Navigation Loading States âŗ

A loading.tsx file automatically provides instant loading UI during navigation to a route that's still fetching data, wrapping it in a React Suspense boundary.

app/dashboard/loading.tsx

export default function Loading() {
  return <p>Loading dashboardâ€Ļ</p>;
}

Tip

You can also use useLinkStatus inside a <Link> to show a per-link pending indicator during transitions.

Navigation with Server Components đŸ–Ĩī¸

Server Components handle navigation-related data through props like params and searchParams, and can trigger server-side navigation with redirect() or notFound() — but they cannot use hooks like useRouter.

app/orders/[id]/page.tsx

import { notFound } from "next/navigation";

export default async function Order({ params }: { params: { id: string } }) {
  const order = await getOrder(params.id);
  if (!order) notFound();
  return <h1>Order #{order.id}</h1>;
}

Navigation with Client Components đŸ’ģ

Client Components unlock the full interactive navigation toolkit — useRouter, usePathname, useSearchParams, and useParams — enabling dynamic, event-driven navigation logic.

components/LogoutButton.tsx

"use client";

import { useRouter } from "next/navigation";

export default function LogoutButton() {
  const router = useRouter();

  return (
    <button onClick={() => { logout(); router.push("/login"); }}>
      Log out
    </button>
  );
}

Protected Navigation đŸ›Ąī¸

Protected navigation restricts access to certain routes based on authentication or authorization state, typically enforced via middleware.ts or checks within Server Components.

middleware.ts

import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const isAuthenticated = request.cookies.has("session");
  if (!isAuthenticated) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Best Practice

Enforce protection at the middleware level whenever possible — it runs before rendering and avoids exposing protected UI even briefly.

Authentication Redirects 🔐

When a user isn't authenticated, the standard pattern is to redirect() them to a login page, often preserving the originally requested path as a query parameter for post-login redirection.

app/dashboard/page.tsx

import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";

export default async function Dashboard() {
  const session = await getSession();
  if (!session) {
    redirect("/login?from=/dashboard");
  }
  return <h1>Welcome back!</h1>;
}

Performance Optimization 🚀

  • Rely on automatic prefetching from <Link> rather than manual data fetching on hover.
  • Use loading.tsx to stream content and avoid blank-screen waits during navigation.
  • Avoid unnecessary router.refresh() calls, which re-fetch Server Component data.
  • Wrap components using useSearchParams in <Suspense> to avoid blocking static rendering.

Best Practices ✅

  • Prefer <Link> for all in-app navigation instead of manual anchor tags.
  • Keep authentication checks close to middleware for the earliest possible redirect.
  • Use useParams and useSearchParams only in Client Components that genuinely need reactivity.
  • Derive active-link state from usePathname instead of manually tracking navigation.

Common Mistakes âš ī¸

  • Calling redirect() inside a try/catch block, which silently swallows the redirect.
  • Forgetting to wrap useSearchParams usage in a <Suspense> boundary.
  • Using next/router (Pages Router) instead of next/navigation (App Router) by mistake.
  • Overusing router.refresh(), causing unnecessary re-fetches of server data.

Frequently Asked Questions đŸ’Ŧ

Question

Does <Link> support external URLs?

Answer

Yes, but external links skip prefetching and client-side transitions — they behave like a normal <a> tag.

Question

Can I use useRouter in a Server Component?

Answer

No — routing hooks are Client Component only; Server Components use redirect() and notFound() instead.

Question

How do I preserve query parameters across navigation?

Answer

Read the current parameters with useSearchParams, merge in your changes, and construct the new href manually.

Summary 📌

Summary

A solid grasp of these navigation tools lets you build fast, intuitive, and secure routing experiences across both Server and Client Components.