Suggest an editImprove this articleRefine the answer for “Next/link and navigation in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`next/link`** is the primary navigation component in Next.js: renders an `<a>` tag but handles routing client-side, no full page reload. ```tsx <Link href="/dashboard">Dashboard</Link> // Programmatic: const router = useRouter() router.replace('/dashboard') // Server-side: redirect('/login') ``` **Key:** use `Link` for UI clicks, `useRouter` after logic runs, and `redirect()` in Server Components.Shown above the full answer for quick recall.Answer (EN)Image**`next/link`** is a React component that handles client-side navigation in Next.js by intercepting anchor clicks, preventing full page reloads, and prefetching routes before the user ever clicks. ## Theory ### TL;DR - `Link` is a smart `<a>` tag: same HTML output, no page reload, automatic prefetch on viewport entry or hover - `useRouter().push()` is for programmatic navigation after logic runs (form submit, API response) - `redirect()` runs on the server, before HTML reaches the client, no `'use client'` needed - `useRouter` from `next/navigation` (App Router) and `next/router` (Pages Router) are different hooks with different APIs - Default `Link` prefetches on viewport entry; use `prefetch={false}` for rarely visited pages ### Quick Example ```tsx // Basic navigation bar - pages/index.tsx import Link from 'next/link' export default function Nav() { return ( <nav> {/* Prefetches /about when link enters the viewport */} <Link href="/about">About</Link> {/* Skip prefetch for rarely visited admin pages */} <Link href="/admin/settings" prefetch={false}>Settings</Link> </nav> ) } // Clicking "About" swaps page content without a full reload ``` The `Link` component renders a regular `<a>` in the DOM. The difference is what happens on click: Next.js intercepts it, calls `event.preventDefault()`, and handles the transition through the router instead of the browser. ### Link vs useRouter vs redirect Three tools, three contexts. `Link` lives in JSX and handles clicks declaratively. `useRouter` gives you a programmatic handle for navigating after async work completes. `redirect()` runs on the server, before any HTML reaches the client. The decision rule is straightforward. If a user clicks something and goes somewhere, use `Link`. If code decides where to go after a form submit or login check, use `useRouter`. If a Server Component or Server Action needs to redirect, use `redirect()`. ### When to Use - Nav bars, cards, breadcrumbs, any clickable element that goes somewhere: `Link` - After form submission, search, login success: `useRouter().push()` - Auth guards in Server Components, post-mutation redirects in Server Actions: `redirect()` - Routes where the back button should skip the current page (modals, login flows): `replace` prop on `Link` or `router.replace()` - Rarely visited or deep admin pages: `Link` with `prefetch={false}` ### Comparison Table | Feature | `Link` | `useRouter().push()` | `redirect()` | |---|---|---|---| | Where it runs | Client (JSX) | Client (event handler) | Server (component/action) | | Prefetch | Automatic | No | N/A | | Adds to history | Yes, default | Yes | N/A | | Needs `'use client'` | No | Yes | No | | Use case | Nav UI | Post-logic navigation | Auth guards, mutations | ### How Prefetching Works When a `Link` enters the viewport (roughly within three viewport heights, per Next.js 14 behavior), Next.js fires a background fetch for that route's RSC payload and caches it in memory. For static routes, it loads the full payload. For dynamic routes, it loads up to the nearest `loading.tsx` boundary. The result: page swaps feel instant, typically under 200ms. On mobile or low-bandwidth connections, too many links prefetching simultaneously can hurt performance. Admin pages and rarely visited routes are good candidates for `prefetch={false}`, with `router.prefetch(url)` called manually on user intent instead. ### Active Links and usePathname ```tsx 'use client' import Link from 'next/link' import { usePathname } from 'next/navigation' export function NavLink({ href, children }: { href: string children: React.ReactNode }) { const pathname = usePathname() return ( <Link href={href} className={pathname === href ? 'font-bold text-blue-600' : 'text-neutral-600'} > {children} </Link> ) } ``` `usePathname` is client-only, so the component needs `'use client'`. The `Link` component itself does not. ### useRouter Methods ```tsx 'use client' import { useRouter } from 'next/navigation' export function SearchBar() { const router = useRouter() function handleSearch(query: string) { router.push(`/search?q=${query}`) // adds to history } return <input onChange={(e) => handleSearch(e.target.value)} /> } ``` | Method | What it does | |---|---| | `router.push(url)` | Navigate to URL, adds to history stack | | `router.replace(url)` | Navigate without adding to history | | `router.back()` | Go back in history | | `router.forward()` | Go forward in history | | `router.refresh()` | Re-fetches current route data from server | | `router.prefetch(url)` | Manually prefetch a route | Note: `useRouter` from `next/navigation` has no `router.query`. For query params in App Router, use `useSearchParams`. ### Server-Side Navigation with redirect ```tsx // app/dashboard/page.tsx - Server Component import { redirect } from 'next/navigation' import { getUser } from '@/lib/auth' export default async function DashboardPage() { const user = await getUser() if (!user) redirect('/login') // throws internally, Next.js handles it if (!user.hasAccess) redirect('/upgrade') return <Dashboard user={user} /> } ``` ```tsx // app/actions.ts - Server Action 'use server' import { redirect } from 'next/navigation' export async function createPost(formData: FormData) { const post = await db.post.create({ data: parseForm(formData) }) redirect(`/posts/${post.id}`) // runs after mutation, no client JS } ``` `redirect()` throws an internal error that Next.js catches, so no `return` statement is needed after it. ### useSearchParams ```tsx 'use client' import { useSearchParams } from 'next/navigation' export function FilterPanel() { const searchParams = useSearchParams() const difficulty = searchParams.get('difficulty') // reads ?difficulty=hard return <p>Filter: {difficulty || 'all'}</p> } ``` Use `useSearchParams` anywhere you need query string values in a Client Component. The App Router has no `router.query`. ### Common Mistakes **Using `<a>` instead of `Link` in App Router** ```tsx // Full page reload, loses scroll restoration and prefetch cache <a href="/dashboard">Dashboard</a> // Correct <Link href="/dashboard">Dashboard</Link> ``` Regular `<a>` bypasses the Next.js router entirely. The browser reloads the page and throws away every cached RSC payload. **Forgetting `replace` on login redirects** ```tsx // After login, push adds /login to history // User hits back and lands back on the login form router.push('/dashboard') // replace overwrites the history entry instead router.replace('/dashboard') // Or declaratively: <Link href="/dashboard" replace>Continue</Link> ``` This is a common production bug. The back button loops the user back to the login page. **Using `useRouter` in Server Components** ```tsx // Throws at runtime. useRouter is client-only. export default async function Page() { const router = useRouter() // Error! } // Correct: use redirect() on the server import { redirect } from 'next/navigation' export default async function Page() { const user = await getUser() if (!user) redirect('/login') } ``` **Mixing up `next/navigation` and `next/router`** I have seen this one kill an entire afternoon of debugging. Both imports compile without errors. The problem shows up at runtime when `router.query` is `undefined` in the App Router. ```tsx // Pages Router only - has router.query import { useRouter } from 'next/router' // App Router - no router.query, use useSearchParams import { useRouter } from 'next/navigation' import { useSearchParams } from 'next/navigation' ``` ### Real-World Usage - Vercel dashboard: `Link` for sidebar navigation between projects and deployments - shadcn/ui nav components: `Link` wrapped with `usePathname` for active state styling - T3 Stack apps: `Link` for protected routes post-signin, `redirect()` in auth callbacks - Filter pages (search, e-commerce): `useRouter().push()` to update URL after user selects filters ### Follow-Up Questions **Q:** What is the difference between `push` and `replace`? **A:** `push` adds a new entry to the browser history stack, so the back button works normally. `replace` overwrites the current entry. Use `replace` for login redirects and modal flows where back should not return to the same page. **Q:** How does prefetching behave for dynamic routes? **A:** For static routes, Next.js fetches the full RSC payload. For dynamic routes, prefetch loads only up to the nearest `loading.tsx` boundary, so the shell renders immediately and data loads after navigation. **Q:** What is the difference between `useRouter` from `next/navigation` and `next/router`? **A:** `next/navigation` is for the App Router (Next.js 13+) and does not have `router.query`. `next/router` is for the Pages Router. Mixing them up compiles fine but breaks at runtime. **Q:** How do you handle loading states during programmatic navigation? **A:** The App Router shows `loading.tsx` automatically on navigation. For finer control, wrap `router.push()` in `useTransition` and use the `isPending` flag to disable buttons or show a spinner while the transition is in progress. **Q:** How does `Link` interact with parallel routes and intercepting routes? **A:** Intercepting routes (`@folder` convention) override `href` resolution before the prefetch fires. Parallel routes load independently, so a `@modal` slot stays mounted while the primary route changes. `Link` navigates only the segment it targets without affecting sibling slots. ## Examples ### Dashboard Navigation with Active States ```tsx // components/DashboardNav.tsx 'use client' import Link from 'next/link' import { usePathname } from 'next/navigation' const navItems = [ { href: '/dashboard', label: 'Overview' }, { href: '/dashboard/analytics', label: 'Analytics' }, { href: '/dashboard/users', label: 'Users', skipPrefetch: true }, ] export default function DashboardNav() { const pathname = usePathname() return ( <nav className="flex gap-4"> {navItems.map(({ href, label, skipPrefetch }) => ( <Link key={href} href={href} prefetch={skipPrefetch ? false : undefined} className={pathname === href ? 'font-bold text-blue-600' : 'text-neutral-600'} > {label} </Link> ))} </nav> ) } // Active item gets bold; /users skips prefetch since it is rarely visited ``` The active state check is a strict equality on the full pathname. For nested routes like `/dashboard/analytics/reports`, you would use `pathname.startsWith(href)` instead. ### Login Form with Programmatic Navigation ```tsx // app/login/page.tsx 'use client' import { useRouter } from 'next/navigation' import { useState } from 'react' export default function LoginPage() { const router = useRouter() const [loading, setLoading] = useState(false) async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() setLoading(true) const formData = new FormData(e.currentTarget) const result = await login(formData) if (result.success) { // replace so the back button skips the login form router.replace('/dashboard') } else { setLoading(false) } } return ( <form onSubmit={handleSubmit}> <input name="email" type="email" /> <input name="password" type="password" /> <button disabled={loading}> {loading ? 'Logging in...' : 'Login'} </button> </form> ) } ``` `router.replace` here prevents the login page from appearing in history after a successful login. Without it, the back button from the dashboard would send the user back to the login form. ### Server-Side Auth Guard ```tsx // app/dashboard/page.tsx import { redirect } from 'next/navigation' import { getUser } from '@/lib/auth' import { Dashboard } from '@/components/Dashboard' export default async function DashboardPage() { const user = await getUser() // Runs on the server, zero client JS involved if (!user) redirect('/login') if (user.plan === 'free') redirect('/upgrade') return <Dashboard user={user} /> } // redirect() throws internally so Next.js handles the response // No explicit return needed after redirect calls ``` This pattern is the standard auth guard in App Router. The check happens before the component renders, the client never receives unauthorized HTML, and no `'use client'` directive is needed.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.