React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

linkuseRouterusePathnameuseSearchParamsredirectnavigationprefetch

Navigation

Navigate between routes using the <Link> component, programmatic useRouter, URL-reading hooks, and server-side redirect.

Recipe

Quick-reference recipe card — copy-paste ready.

// Declarative navigation
import Link from "next/link";
<Link href="/dashboard">Dashboard</Link>
 
// Programmatic navigation (Client Component)
"use client";
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/dashboard");
 
// Read current URL (Client Component)
import { usePathname, useSearchParams } from "next/navigation";
const pathname = usePathname();        // "/dashboard"
const searchParams = useSearchParams(); // URLSearchParams instance
 
// Server-side redirect (Server Component or Server Action)
import { redirect } from "next/navigation";
redirect("/login");

When to reach for this: Any time you need to move between pages, read the current URL, or redirect users based on conditions.

Working Example

// components/nav-bar.tsx — Navigation with active link highlighting
"use client";
 
import Link from "next/link";
import { usePathname } from "next/navigation";
 
const links = [
  { href: "/", label: "Home" },
  { href: "/dashboard", label: "Dashboard" },
  { href: "/settings", label: "Settings" },
];
 
export function NavBar() {
  const pathname = usePathname();
 
  return (
    <nav className="flex gap-4 border-b px-6 py-3">
      {links.map((link) => (
        <Link
          key={link.href}
          href={link.href}
          className={
            pathname === link.href
              ? "font-bold text-blue-600"
              : "text-gray-600 hover:text-gray-900"
          }
        >
          {link.label}
        </Link>
      ))}
    </nav>
  );
}
// components/search-filter.tsx — Search params for filtering
"use client";
 
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useCallback } from "react";
 
export function SearchFilter() {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const router = useRouter();
 
  const currentQuery = searchParams.get("q") ?? "";
  const currentSort = searchParams.get("sort") ?? "newest";
 
  const updateParams = useCallback(
    (key: string, value: string) => {
      const params = new URLSearchParams(searchParams.toString());
      if (value) {
        params.set(key, value);
      } else {
        params.delete(key);
      }
      router.push(`${pathname}?${params.toString()}`);
    },
    [searchParams, pathname, router]
  );
 
  return (
    <div className="flex gap-4">
      <input
        type="text"
        placeholder="Search..."
        defaultValue={currentQuery}
        onChange={(e) => updateParams("q", e.target.value)}
        className="rounded border px-3 py-2"
      />
      <select
        value={currentSort}
        onChange={(e) => updateParams("sort", e.target.value)}
        className="rounded border px-3 py-2"
      >
        <option value="newest">Newest</option>
        <option value="oldest">Oldest</option>
        <option value="popular">Popular</option>
      </select>
    </div>
  );
}
// app/login/page.tsx — Server-side redirect after auth check
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
 
export default async function LoginPage() {
  const session = await auth();
  if (session) redirect("/dashboard");
 
  return (
    <form action="/api/auth/login" method="POST">
      <input name="email" type="email" placeholder="Email" />
      <input name="password" type="password" placeholder="Password" />
      <button type="submit">Log In</button>
    </form>
  );
}
// Programmatic navigation after form submission
"use client";
 
import { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
 
export function CreatePostForm() {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [title, setTitle] = useState("");
 
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const res = await fetch("/api/posts", {
      method: "POST",
      body: JSON.stringify({ title }),
      headers: { "Content-Type": "application/json" },
    });
    const post = await res.json();
 
    startTransition(() => {
      router.push(`/blog/${post.slug}`);
      router.refresh(); // Revalidate server data
    });
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
        className="rounded border px-3 py-2"
      />
      <button
        type="submit"
        disabled={isPending}
        className="ml-2 rounded bg-blue-600 px-4 py-2 text-white"
      >
        {isPending ? "Creating..." : "Create"}
      </button>
    </form>
  );
}

Deep Dive

How It Works

  • <Link> prefetches by default. When a <Link> enters the viewport, Next.js prefetches the route in the background. Static routes are fully prefetched; dynamic routes prefetch up to the nearest loading.tsx boundary.
  • <Link> performs client-side navigation. It intercepts the click, updates the URL, and renders the new route without a full page reload. Only the changed segments re-render.
  • useRouter provides imperative navigation. push(), replace(), back(), forward(), and refresh() let you navigate programmatically.
  • router.refresh() re-fetches server data. It revalidates the current route's Server Components without losing client-side state (input values, scroll position).
  • usePathname() returns the current path. It is reactive — the component re-renders when the URL changes. It does not include search params or hash.
  • useSearchParams() returns a read-only URLSearchParams. To update search params, construct a new URL string and use router.push().
  • redirect() throws internally. It must be called outside try/catch blocks (or use unstable_rethrow in the catch). Code after redirect() never runs.
  • redirect() defaults to 307 (temporary). Use redirect(url, RedirectType.replace) for 308 (permanent) or use permanentRedirect().
  • Navigation hooks require Client Components. useRouter, usePathname, and useSearchParams all require the "use client" directive.

Variations

// Link with dynamic href and prefetch control
import Link from "next/link";
 
// Disable prefetch for rarely visited links
<Link href="/terms" prefetch={false}>Terms</Link>
 
// Dynamic href with template literal
<Link href={`/blog/${slug}`}>Read More</Link>
 
// Link with replace (no new history entry)
<Link href="/dashboard" replace>Dashboard</Link>
 
// Link with scroll={false} to preserve scroll position
<Link href="/dashboard?tab=settings" scroll={false}>Settings Tab</Link>
// permanentRedirect — 308 redirect
import { permanentRedirect } from "next/navigation";
 
export default async function OldPage() {
  permanentRedirect("/new-page");
}
// redirect in Server Actions
"use server";
 
import { redirect } from "next/navigation";
 
export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: { title: formData.get("title") as string },
  });
  redirect(`/blog/${post.slug}`);
}
// useSelectedLayoutSegment — read active child segment
"use client";
 
import { useSelectedLayoutSegment } from "next/navigation";
 
export function TabNav() {
  const segment = useSelectedLayoutSegment();
  // On /dashboard/analytics → segment = "analytics"
  // On /dashboard → segment = null
 
  return (
    <nav>
      <Link
        href="/dashboard"
        className={segment === null ? "font-bold" : ""}
      >
        Overview
      </Link>
      <Link
        href="/dashboard/analytics"
        className={segment === "analytics" ? "font-bold" : ""}
      >
        Analytics
      </Link>
    </nav>
  );
}

TypeScript Notes

// Link component props
import type { LinkProps } from "next/link";
// Key props: href (string or UrlObject), replace, scroll, prefetch
 
// useRouter return type
import { useRouter } from "next/navigation";
// router.push(href: string, options?: { scroll?: boolean }): void
// router.replace(href: string, options?: { scroll?: boolean }): void
// router.refresh(): void
// router.back(): void
// router.forward(): void
// router.prefetch(href: string): void
 
// redirect function signature
import { redirect, permanentRedirect, RedirectType } from "next/navigation";
// redirect(url: string, type?: RedirectType): never
// permanentRedirect(url: string, type?: RedirectType): never
 
// URL object for complex hrefs
<Link
  href={{
    pathname: "/blog/[slug]",
    query: { slug: "hello-world" },
  }}
>
  Read Post
</Link>

Gotchas

  • useRouter is from next/navigation, not next/router. The next/router version is for the Pages Router and will not work in App Router.
  • redirect() throws an error internally. If called inside a try/catch, it will be caught. Use unstable_rethrow(error) in catch blocks that might intercept it.
  • useSearchParams() must be wrapped in <Suspense>. In Next.js 15+, using useSearchParams() without a Suspense boundary causes the entire route to opt into client-side rendering. Wrap the component using it in <Suspense>.
  • router.push() does not await. Navigation is asynchronous but the method returns void. Use useTransition to track pending state.
  • <Link> prefetches on viewport entry. This can cause unexpected network requests. Use prefetch={false} for links users rarely click.
  • router.refresh() does not clear the router cache. It only re-fetches the current page's server data. Other cached routes remain stale.
  • searchParams changes do not trigger loading.tsx. Only segment changes (different path) trigger the loading boundary. Search param changes re-render without loading UI.
  • redirect() in a layout applies to all child pages. Be careful placing conditional redirects in layouts — they run for every child route.
  • The scroll prop on <Link> defaults to true. Navigation scrolls to top by default. Set scroll={false} for tab-like navigation or filtered lists.

Alternatives

ApproachWhen to Use
<a> tagExternal links or when you need a full page reload
window.locationEscaping the SPA — full reload to a different URL
Middleware redirectRedirecting before any rendering occurs
next.config.js redirectsStatic redirect rules without runtime logic
Server Action with redirect()Redirecting after a mutation
revalidatePath or revalidateTagRefreshing data without navigation

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: Centralized URL builder
// File: src/lib/url-builder.ts
import { generateSlug } from './slug-utils';
 
export function buildServiceUrlFromSlug(slug: string): string {
  return `/services/${slug}`;
}
 
export function buildSectionUrl(serviceSlug: string, sectionId: string): string {
  const cleanSectionId = stripServicePrefix(sectionId, serviceSlug);
  return `/services/${serviceSlug}/${cleanSectionId}`;
}
 
export function buildPointUrl(
  serviceSlug: string,
  sectionId: string,
  pointSlug: string
): string {
  const cleanSectionId = stripServicePrefix(sectionId, serviceSlug);
  const cleanPointSlug = stripSectionPrefix(pointSlug, sectionId);
  return `/services/${serviceSlug}/${cleanSectionId}/pt/${cleanPointSlug}`;
}
 
export function buildPointTabUrl(
  serviceSlug: string,
  sectionId: string,
  pointSlug: string,
  tabSlug: string | null
): string {
  const baseUrl = buildPointUrl(serviceSlug, sectionId, pointSlug);
  if (tabSlug === null) return baseUrl;
  return `${baseUrl}/${tabSlug}`;
}
 
export function parseServiceUrl(url: string): string | null {
  const match = url.match(/^\/services\/([^\/]+)\/?$/);
  return match ? match[1] : null;
}

What this demonstrates in production:

  • Each function builds one level of the URL hierarchy: service, section, point, tab
  • This file is the ONLY place where URL patterns like /services/... are defined. Changing the pattern here updates the entire app
  • stripServicePrefix and stripSectionPrefix clean up database IDs that may contain redundant prefixes (legacy data normalization)
  • parseServiceUrl is the inverse, extracting a slug from a URL using regex
  • The pt/ segment in point URLs distinguishes points from sections in the routing hierarchy
  • Never use template strings for URLs elsewhere. Centralized builders prevent broken links when URL patterns change

FAQs

What is the difference between Link and useRouter for navigation?
  • <Link> is declarative, prefetches by default, and is the standard approach for most navigation
  • useRouter is imperative, used for programmatic navigation after events like form submissions
  • Both perform client-side navigation without full page reloads
Gotcha: Which useRouter import should you use in the App Router?

Import from next/navigation, not next/router. The next/router version is for the Pages Router and will not work in App Router components.

How does Link prefetching work?

When a <Link> enters the viewport, Next.js prefetches the route in the background. Static routes are fully prefetched. Dynamic routes prefetch up to the nearest loading.tsx boundary. Use prefetch={false} for rarely visited links.

What does router.refresh() do?

It re-fetches the current route's Server Components without losing client-side state (input values, scroll position). It does not clear the router cache for other routes.

Gotcha: What happens if redirect() is called inside a try/catch?

redirect() throws an error internally, so the catch block intercepts it. Use unstable_rethrow(error) in catch blocks that might accidentally catch redirect() or notFound().

Why must useSearchParams be wrapped in Suspense?

In Next.js 15+, using useSearchParams() without a <Suspense> boundary causes the entire route to opt into client-side rendering. Wrap the component using it in <Suspense>.

How do you update search params without a full navigation?
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
 
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
 
const params = new URLSearchParams(searchParams.toString());
params.set("sort", "newest");
router.push(`${pathname}?${params.toString()}`);
Do search param changes trigger loading.tsx?

No. Only segment changes (different path) trigger the loading boundary. Search param changes re-render the page without showing the loading UI.

What is the difference between redirect() and permanentRedirect()?
  • redirect() sends a 307 (temporary) redirect by default
  • permanentRedirect() sends a 308 (permanent) redirect
  • Both throw internally and code after them never runs
What are the TypeScript types for useRouter methods?
import { useRouter } from "next/navigation";
 
const router = useRouter();
// router.push(href: string, options?: { scroll?: boolean }): void
// router.replace(href: string, options?: { scroll?: boolean }): void
// router.refresh(): void
// router.back(): void
// router.forward(): void
// router.prefetch(href: string): void
How do you track pending state during programmatic navigation?

Use useTransition since router.push() returns void and does not await.

const [isPending, startTransition] = useTransition();
startTransition(() => {
  router.push("/dashboard");
});
// isPending is true while navigating
What does the scroll prop on Link do?

It defaults to true, scrolling to the top of the page on navigation. Set scroll={false} for tab-like navigation or filtered lists where you want to preserve scroll position.

How does useSelectedLayoutSegment help with active link highlighting?
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
 
export function TabNav() {
  const segment = useSelectedLayoutSegment();
  // /dashboard/analytics -> "analytics"
  // /dashboard -> null
  return (
    <Link
      href="/dashboard/analytics"
      className={segment === "analytics" ? "font-bold" : ""}
    >
      Analytics
    </Link>
  );
}