Navigation: Link & Programmatic API
Navigation is like a subway system—there are automated lines (Link components), options for manual control (programmatic navigation), express trains for quick commutes (soft navigation), and transfer routes (hard navigation).
1. What You'll Learn
- Core uses of the
<Link>component:prefetch,scroll,replace useRouterProgrammatic Navigation:push,replace,back,forward,refresh- Use Cases for the
redirect()Function and the<Redirect>Component - The Principles and Differences Between Soft Navigation and Hard Navigation
- Implementation Options for Navigation Loading Indicators
2. A True Story of a Junior Developer
(1) Pain Point: Poor navigation experience between pages
Charlie has recently been developing an e-commerce website using Next.js and has found that the navigation experience is poor:
"When a user clicks a product link from the home page, the page goes blank for 1 second before loading. Every time the user clicks 'Back,' the entire page reloads. Plus, there's a bug—if a user fills out half a form, accidentally clicks a product link, and then clicks 'Back,' all the form data is lost."
Specific question:
| Issue | Impact | User Feedback |
|---|---|---|
| Blank screen when switching pages | Clunky navigation | "It takes forever to click on a link" |
| Form Data Loss | User Re-entered Data | "I filled it out halfway, but when I went back, it was gone" |
| Preloading is not smart | Does not preload frequently visited pages | "The slowest transition is from the home page to the product details page" |
| Scroll Position Reset | Return to the top after paginating on the list page | "Have to scroll back down every time" |
(2) Solutions for Next.js Navigation
Link component auto-fetch + Soft Navigation state preservation + scroll={false} to preserve scroll position.
// Optimized Navigation on the Product List Page
<Link
href={`/products/${product.id}`}
prefetch={true}
scroll={false}
className="block p-4 border rounded hover:shadow"
>
{product.title}
</Link>
(3) Revenue
| Dimension | Before Optimization (Standard <a> Tag) |
After Optimization (<Link> Component) |
|---|---|---|
| Page Load Speed | 1–2 seconds (blank screen) | Instant (pre-fetched cache) |
| Form Data Persistence | Lost when the page is unloaded | Persisted in Soft Nav |
| Scroll Position Retained | Back to Top | Exact Retention |
| User Experience Rating | 3.2/5 | 4.8/5 |
3. Link Component
(1) Basic Usage
graph LR
A[Link Components] --> B[Client Navigation<br/>No full-page refresh]
A --> C[Automatic Pre-fetching<br/>viewport Links within]
A --> D[Scroll Control<br/>scroll={false}]
A --> E[Replacement History<br/>replace]
style A fill:#cce5ff
style B fill:#d4edda
▶ Example: Link Basic Navigation
Output:
Diagram of client-side navigation using Link component.
// ============================================
// Link Basic Usage of Components
// ============================================
import Link from "next/link";
export default function Navigation() {
return (
<nav className="flex gap-6 p-4 bg-white shadow-sm">
{/* Basic Navigation */}
<Link href="/" className="text-blue-600 hover:underline">
Home
</Link>
{/* Dynamic Routing */}
<Link href="/products/42" className="text-blue-600 hover:underline">
Product 42
</Link>
{/* With query parameters */}
<Link
href="/products?category=electronics&sort=price"
className="text-blue-600 hover:underline"
>
Electronics
</Link>
{/* Complete URL */}
<Link
href="https://help.example.com"
className="text-blue-600 hover:underline"
>
Help Center
</Link>
</nav>
);
}
Output:
Renders: Navigation links using Next.js Link component for client-side routing.
Output:
Browser Display 4 A blue link:
Home → /
Product 42 → /products/42
Electronics → /products?category=electronics&sort=price
Help Center → https://help.example.com(External Links)
(2) prefetch
By default, the Link component preloads links within the viewport. The preloading behavior differs between the server and the client:
| Environment | prefetch={true} |
prefetch={false} |
|---|---|---|
| Server-side rendering | Prefetch page data and RSC payload | Do not prefetch |
| Static Page | Preload Entire Page | Do Not Preload |
| Within the viewport | Default behavior | Never preload |
▶ Example: Controlling prefetch Behavior
Output:
The page renders as described above, with the UI updating based on the described behavior.
// ============================================
// Control Link Prefetch Behavior
// ============================================
import Link from "next/link";
export default function ProductList() {
const products = [
{ id: 1, name: "Wireless Mouse" },
{ id: 2, name: "Keyboard" },
{ id: 3, name: "Monitor" },
];
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Products</h1>
{/* Default prefecth:viewport Automatically preload links within the page */}
{products.map(p => (
<Link
key={p.id}
href={`/products/${p.id}`}
className="block p-4 border-b hover:bg-gray-50"
>
{p.name}
</Link>
))}
{/* Turn off prefetching:Suitable for pages that aren't used very often */}
<Link
href="/archive"
prefetch={false}
className="block mt-4 text-gray-500"
>
View Archive (older products)
</Link>
{/* Forced Prefetch:Suitable for the next page you're about to visit */}
<Link
href="/checkout"
prefetch={true}
className="block mt-4 px-6 py-2 bg-blue-600 text-white text-center rounded"
>
Proceed to Checkout
</Link>
</div>
);
}
Output:
Maps over products to render list.
Content: Products
Output:
After the page loads,This can be observed in the network console.:
1. /products/1, /products/2, /products/3 Pre-fetched (within the viewport)
2. /archive Not pre-fetched(prefetch={false})
3. /checkout Pre-fetched(prefetch={true},Even when not there viewport)
When a user clicks the link:
- Pre-fetched pages → Real-time Display(Read from cache)
- Pages Not Prefetched → Displayed after the network request
(3) Scroll Controls
graph LR
A[Navigation Trigger] --> B{scroll Properties}
B -->|scroll=true Default| C[Scroll to the top of the new page]
B -->|scroll=false| D[Keep the current scroll position]
style C fill:#f8d7da
style D fill:#d4edda
▶ Example: scroll={false} to maintain the scroll position
Output:
Diagram: Navigation Trigger; Scroll to the top of the new page; Keep the current scroll position.
// ============================================
// Product List + Modal Window:scroll=false Keep the list in place
// Users click on a product to view its details,The list does not scroll when returning
// ============================================
import Link from "next/link";
export default function ProductGrid() {
const products = Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
name: `Product ${i + 1}`,
}));
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-6">Product Catalog</h1>
<div className="grid grid-cols-4 gap-4">
{products.map(p => (
<Link
key={p.id}
href={`/products/${p.id}`}
scroll={false}
className="border p-4 rounded hover:shadow-lg"
>
<div className="h-32 bg-gray-100 rounded" />
<p className="mt-2 font-medium">{p.name}</p>
</Link>
))}
</div>
</div>
);
}
Output:
Maps over products to render list.
Content: Product Catalog
Output:
1. The user has scrolled to the 3 Items per Page(Scrolled down 2000px)
2. Click on the 15 Go to the product details page
3. Click the browser's Back button
4. Back to the list page,Keep the scroll position at 2000px(No "Back to Top")
5. Comparison:If not scroll={false},Return to the top each time
(4) replace: Replace history
| Action | push (default) |
replace |
|---|---|---|
| Browser History | Add a new entry | Replace the current entry |
| Back Button Behavior | Return to the previous page | Go to the page that was replaced |
| After form submission | Cannot return to the form page | Can return (bypassing the form page) |
▶ Example: Using replace in a form scenario
Output:
The component renders the described UI in the browser.
// ============================================
// replace Replacement History:You cannot return to the form page after submitting.
// ============================================
import Link from "next/link";
export default function CheckoutPage() {
return (
<div className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-6">Checkout</h1>
<div className="space-y-4">
<input placeholder="Card number" className="w-full p-3 border rounded" />
<input placeholder="Expiry date" className="w-full p-3 border rounded" />
<input placeholder="CVV" className="w-full p-3 border rounded" />
</div>
{/* Usage replace:Once submitted, you cannot return to this page. */}
<Link
href="/order-confirmation"
replace
className="block mt-6 w-full p-3 bg-blue-600 text-white text-center rounded"
>
Place Order
</Link>
<p className="text-sm text-gray-500 mt-2 text-center">
After placing order, back button will skip this page
</p>
</div>
);
}
Output:
Content: Checkout | Place Order | After placing order, back button wi
Output:
User Flow:
1. Home → Shopping Cart → Checkout → Confirmation Page
2. Tap on the checkout page "Place Order"
3. The browser address changes to /order-confirmation
4. The user clicks the Back button
5. Skip the checkout page,Go directly to the shopping cart page
6. Prevent users from accidentally returning to the checkout page and submitting the order again
4. Programmatic Navigation with useRouter
(1) API Quick Reference
graph TB
A[useRouter] --> B[push(url) - Navigate to a new page]
A --> C[replace(url) - Replace Current History]
A --> D[back() - Back]
A --> E[forward() - Forward]
A --> F[refresh() - Refresh this page]
A --> G[prefetch(url) - Programmatic Prefetching]
style A fill:#cce5ff
| Method | Parameters | Description | Browser Behavior |
|---|---|---|---|
push |
href: string |
Navigate to the new URL | Add to history |
replace |
href: string |
Replace Current History | Replace History |
back |
None | Browser Back | Same as history.back() |
forward |
None | Browser Forward | Same as history.forward() |
refresh |
None | Refresh this page | Server-Side Rendering (RSC) |
prefetch |
href: string |
Prefetch Page | Cache RSC Payload |
▶ Example: Programmatic Navigation
Output:
Diagram of client-side navigation using Link component.
// ============================================
// useRouter Complete Example of Programmatic Navigation
// ============================================
'use client';
import { useRouter } from "next/navigation";
export default function NavigationBar({ userId }: { userId: string }) {
const router = useRouter();
return (
<div className="p-4 bg-white shadow-sm">
<div className="flex gap-4 max-w-4xl mx-auto">
{/* push:Go to the Home Page */}
<button
onClick={() => router.push("/")}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Home
</button>
{/* push:Dynamic Routing */}
<button
onClick={() => router.push(`/users/${userId}`)}
className="px-4 py-2 bg-gray-600 text-white rounded"
>
Profile
</button>
{/* replace:Replace the current page */}
<button
onClick={() => router.replace("/login")}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Logout
</button>
{/* back/forward:Browsing History */}
<button
onClick={() => router.back()}
className="px-4 py-2 border rounded"
>
← Back
</button>
<button
onClick={() => router.forward()}
className="px-4 py-2 border rounded"
>
Forward →
</button>
{/* refresh:Refresh this page(Server-Side Re-rendering) */}
<button
onClick={() => router.refresh()}
className="px-4 py-2 border rounded"
>
Refresh ↻
</button>
</div>
</div>
);
}
Output:
NavigationBar renders its UI.
Output:
Click each button:
[Home] → Navigate to /
[Profile] → Navigate to /users/123
[Logout] → Replace the current history with /login(No Return)
[← Back] → Browser Back Button
[Forward] → Browser Forward
[Refresh] → Refresh this page(RSC Render Again,Does not cause the entire page to reload)
▶ Example: Navigation After Form Submission
Output:
The page renders as described above, with the UI updating based on the described behavior.
// ============================================
// Form Submission:Verification → save → Programmatic Navigation
// ============================================
'use client';
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function CreateProjectForm() {
const router = useRouter();
const [saving, setSaving] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
try {
// Simulation API Call
await new Promise(resolve => setTimeout(resolve, 1500));
// After successful completion, navigate to the new project page
router.push("/projects/42");
// and refresh the data cache
router.refresh();
} catch (err) {
console.error("Failed to create project:", err);
alert("Failed to create project. Please try again.");
} finally {
setSaving(false);
}
}
return (
<form onSubmit={handleSubmit} className="max-w-lg mx-auto p-8 space-y-4">
<h1 className="text-2xl font-bold">Create Project</h1>
<input
name="name"
placeholder="Project name"
className="w-full p-3 border rounded"
required
/>
<textarea
name="description"
placeholder="Description"
className="w-full p-3 border rounded h-32"
/>
<button
type="submit"
disabled={saving}
className="w-full p-3 bg-blue-600 text-white rounded disabled:opacity-50"
>
{saving ? "Creating..." : "Create Project"}
</button>
</form>
);
}
Output:
Form with: name.
Content: Create Project
Output:
1. Users enter the project name and description
2. Click the "Create Project" button
3. The button changes to "Creating..."(Disabled)
4. After 1.5s (simulated API delay)
5. Page Navigation to /projects/42
6. Refresh Project List Data(Includes newly created projects)
5. redirect() and <Redirect>
(1) Two Types of Redirects
graph TB
A[Redirect Requests] --> B[Server-side<br/>redirect()]
A --> C[Client<br/><Redirect>]
B --> D[Server Action / Route Handler]
B --> E[Server Component]
C --> F[Client Component]
C --> G[When using conditional rendering]
style B fill:#d4edda
style C fill:#cce5ff
| Method | Where to Use | When to Trigger | Performance |
|---|---|---|---|
redirect() |
Server Actions / Server Components | During Server Response | Server Redirection, Zero Client Overhead |
<Redirect> |
Client Components | During rendering | Client-side redirection, route changes |
6. Soft vs Hard Navigation
(1) Comparison
graph TB
subgraph "Soft Navigation(Link/useRouter)"
A[Client-Side Routing] --> B[Page Navigation<br/>No full-page refresh]
B --> C[layout Keep Mounted]
B --> D[React State Preservation]
B --> E[RSC Payload Replace]
end
subgraph "Hard Navigation(Refresh the entire browser page)"
F[Full-Page Loading in the Browser] --> G[Page Navigation<br/>Refresh the entire page]
G --> H[layout Remount]
G --> I[Reset All States]
G --> J[Full Set JS/CSS Loading]
end
style A fill:#d4edda
style F fill:#f8d7da
| Characteristic | Soft Navigation | Hard Navigation |
|---|---|---|
| Trigger Method | <Link>, useRouter() |
Browser refresh, <a> tab, window.location |
| Refresh Entire Page | ❌ No | ✅ Yes |
| Layout Preserved | ✅ Preserved | ❌ Remounted |
| RSC Payload | Incremental Update | Full Download |
| Performance | Real-time | 500 ms–2 s |
| Status Maintained | ✅ | ❌ |
7. Navigation Loading Indicator
(1) Implementation Plan
graph TB
A[Navigation Trigger] --> B[Loading Indicator]
B --> C[Top Progress Bar<br/>NProgress Style]
B --> D[Suspense fallback<br/>Wireframe Display]
B --> E[loading.tsx<br/>Page-Level Loading]
style A fill:#cce5ff
style B fill:#f8d7da
8. Complete Example: E-commerce Navigation System
// ============================================
// Comprehensive Example:A Comprehensive Navigation System for E-commerce
// Covering Link、useRouter、redirect、Navigation Loading State
// ============================================
// src/app/layout.tsx — Global Navigation Bar
import Link from "next/link";
import { CartCount } from "./CartCount";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header className="bg-white shadow-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<Link href="/" className="text-2xl font-bold text-blue-600">
ShopHub
</Link>
<nav className="hidden md:flex gap-6">
<Link href="/products" className="hover:text-blue-600">
Products
</Link>
<Link href="/categories" className="hover:text-blue-600">
Categories
</Link>
<Link href="/deals" prefetch={false} className="hover:text-blue-600">
Deals
</Link>
<Link href="/about" className="hover:text-blue-600">
About
</Link>
</nav>
<div className="flex items-center gap-4">
<Link href="/search" className="text-gray-600 hover:text-blue-600">
Search
</Link>
<Link href="/cart" className="relative text-gray-600 hover:text-blue-600">
Cart
<CartCount />
</Link>
<Link
href="/account"
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm"
>
Sign In
</Link>
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-4 py-8">
{children}
</main>
</body>
</html>
);
}
// src/app/products/[id]/page.tsx — Product Details(Includes navigation logic)
'use client';
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function ProductDetail({ params }) {
const router = useRouter();
async function handleAddToCart() {
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId: params.id }),
});
// Refresh the quantity in the shopping cart after adding an item
router.refresh();
}
async function handleBuyNow() {
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId: params.id, quantity: 1 }),
});
// Buy Now → Proceed to Checkout (use replace to avoid returning to the product page)
router.replace("/checkout");
}
return (
<div>
{/* Breadcrumb Navigation */}
<nav className="text-sm text-gray-500 mb-6">
<Link href="/" className="hover:text-blue-600">Home</Link>
<span className="mx-2">/</span>
<Link href="/products" className="hover:text-blue-600">Products</Link>
<span className="mx-2">/</span>
<span className="text-gray-900">{params.id}</span>
</nav>
<div className="flex gap-8">
<div className="w-1/2">
<img
src={`https://picsum.photos/seed/${params.id}/400/400`}
alt="Product"
className="w-full rounded-lg"
/>
</div>
<div className="w-1/2">
<h1 className="text-3xl font-bold">Product #{params.id}</h1>
<p className="text-2xl text-green-600 font-bold mt-4">$49.99</p>
<p className="text-gray-600 mt-4">
High-quality product with premium features.
</p>
<div className="flex gap-4 mt-8">
<button
onClick={handleAddToCart}
className="flex-1 px-6 py-3 border-2 border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50"
>
Add to Cart
</button>
<button
onClick={handleBuyNow}
className="flex-1 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Buy Now
</button>
</div>
<div className="mt-8 border-t pt-6">
<Link
href={`/products/${Number(params.id) - 1}`}
scroll={false}
className="text-blue-600 hover:underline"
>
← Previous Product
</Link>
<Link
href={`/products/${Number(params.id) + 1}`}
scroll={false}
className="text-blue-600 hover:underline float-right"
>
Next Product →
</Link>
</div>
</div>
</div>
</div>
);
}
// src/app/CartCount.tsx — Shopping Cart Quantity Display
export async function CartCount() {
const cart = await fetch("https://api.example.com/cart");
const itemCount = cart?.items?.length ?? 0;
if (itemCount === 0) return null;
return (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
{itemCount}
</span>
);
}
Expected Output:
Navigation Bar(Fixed at the top):
[ShopHub] Products Categories Deals About [Search] Cart(3) [Sign In]
Product Details Page(/products/1):
Home / Products / 1
[Product Images] [Product Details]
Product #1
$49.99
[Add to Cart] [Buy Now]
← Previous Product | Next Product →
Navigation Behavior:
- Click Products → Soft Nav,Instant Page Switching
- Click Deals → No advance pickup(prefetch=false),But navigation is still fast
- Add to Cart → router.refresh(),Shopping Cart Logo Update
- Buy Now → router.replace("/checkout"),Replacement History
❓ FAQ
Link component and the <a> tag?Link component handles routing on the client side without triggering a full page refresh, preserves the layout, and supports preloading. The <a> tag triggers a full page reload (hard navigation), causing all React state to be lost. Next.js uses Link for all internal routing.useRouter need to include 'use client'?useRouter is a React hook and can only be used in client components. Server components do not have access to browser APIs (such as history and location), so routing-related hooks are executed on the client side.router.refresh() and window.location.reload()?router.refresh() is Next.js’s soft refresh—it re-requests the server-side RSC payload, updating only the changed parts while preserving client-side state (such as useState and Context). window.location.reload() performs a full page refresh, causing all state to be lost and requiring a full reload of JS and CSS.prefetch={false}.redirect() and <Redirect> be used?redirect() is used on the server side (Server Actions, Server Components, Route Handlers) to send a 303/307 response. For client-side conditional redirection, use access control logic (display an “Access Denied” message + a navigation button) instead of the <Redirect> component.router.refresh() to refresh the server-side data; 3) Check whether the data is being cached by the layout.📖 Summary
- The
<Link>component is the preferred navigation solution for Next.js, supporting automatic preloading and client-side transitions prefetch={true}Preload links within the viewport,prefetch={false}Disable preloadingscroll={false}Maintains scroll position; suitable for list pages and detail pagesreplaceOverride the browser history to prevent accidental navigation back from form pagesuseRouterProvides programmatic navigation:push,replace,back,forward,refreshredirect()For use on the server side; sends a 303/307 redirect response- Soft Navigation preserves the layout and state, while Hard Navigation refreshes the entire page
router.refresh()is a special soft navigation feature: it refreshes RSC data without changing the URL
📝 Exercises
-
Basic Question (⭐): Create five links on the page (Home, About, Contact, Product List, Product Details #42), and observe which URLs are preloaded in the browser’s Network tab.
-
Advanced Exercise (⭐⭐): Implement a "Login → Dashboard" workflow: Use a Server Action to verify the login; if successful, use
redirect()to redirect to the dashboard; if unsuccessful, useredirect()to return to the login page and include the error parameters. -
Challenge (⭐⭐⭐): Implement an infinite-scrolling list page. When a user clicks on a product to go to the details page, use
scroll={false}to preserve the scroll position; when the user returns, the list should remain at its previous position. Userouter.back()to implement the back navigation.



