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
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
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>
);
}| Prop | Purpose |
|---|---|
| href | destination path (required) |
| prefetch | enable or disable prefetching |
| replace | replace history instead of pushing |
| scroll | control 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
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
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
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>;
}| Method | Behavior |
|---|---|
| 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
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.
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 value | Behavior |
|---|---|
| true (default for static routes) | full route prefetched |
| null (default for dynamic routes) | only the loading state is prefetched |
| false | prefetching 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
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
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.