Navigation
A user fills out half a form, clicks a nav link to check something, hits back—and their form data is gone. Full page reload. Or they're scrolled halfway down a list, navigate to a detail page, come back—scroll position reset to top. These aren't bugs in your code; they're the default behavior of plain <a> tags.
next/link enables soft navigation: client-side transitions that preserve state, maintain scroll position, and feel instant.
VIDEO PLACEHOLDER · 3-4 min · EVERGREEN · Low Priority
Soft Navigation: What Happens Under the Hood
Side-by-side comparison of hard navigation (full page reload) vs soft navigation (client-side transition). Shows network requests, what gets preserved (layouts, state), and why this matters for perceived performance.
Outcome
Meaningful links that soft-navigate and preserve state where needed.
Fast Track
- Replace plain anchors with
next/linkwhere appropriate. - Verify soft navigation behavior.
- Handle focus management and accessibility.
Hands-On Exercise 2.6
Implement soft navigation with proper state preservation.
Requirements:
- Audit key navigations and swap to
next/link. - Ensure focus lands meaningfully after navigation.
- Confirm back/forward browser history behavior is intact.
- Demonstrate both client-side (useRouter) and server-side (redirect) navigation.
Key Concepts:
- Soft navigation: Client-side route transitions without full page reload. The URL updates via JavaScript, only new page content is fetched, and shared layouts persist. State, scroll position, and interactivity are maintained.
- Hard navigation: Traditional full page load (triggered by plain
<a>tags or external navigation). Clears application state, resets scroll position, and causes a full browser refresh.
Implementation hints:
- next/link provides soft navigation: No full page reloads, JavaScript updates URL, fetches only new page content, shared layouts persist.
- Link behavior: Prefetching, client-side transitions, preserves state across navigation.
- Back button: Browser navigation respects history; state is preserved.
- useRouter (Client): For programmatic soft navigation (
router.push(),router.back(),router.replace()), user interactions, conditional navigation. - redirect() (Server): For server-side navigation, authentication checks, form submissions after success.
- Avoid client boundaries when not needed.
- Keep link text accessible and descriptive.
Use useRouter from 'next/navigation' for client-side programmatic navigation (user interactions). Use redirect() from 'next/navigation' for server-side navigation (auth checks, Server Actions).
Always prefer <Link> over <a> for internal navigation. Link provides soft navigation with fast client-side transitions, preserves component state, and enables prefetching. Plain anchors trigger hard navigation with full page reloads.
I need to implement navigation in Next.js and want to choose the right approach.
<context>
Next.js offers multiple navigation methods:
- `<Link>` component - declarative, for user clicks
- `useRouter().push/replace` - programmatic, client-side
- `redirect()` - server-side, in Server Components/Actions
- `useRouter().back/forward` - history navigation
</context>
<my-scenario>
**What triggers the navigation:** _____
Example: "User clicks a button" or "Form submission succeeds" or "Auth check fails"
**Where the code runs:**
- [ ] Server Component
- [ ] Client Component
- [ ] Server Action
- [ ] Route Handler
**User experience requirements:**
- Should URL change? _____
- Should user be able to go back? _____
- Should form/scroll state be preserved? _____
- Must work without JavaScript? _____
</my-scenario>
<my-current-code>
```tsx
// What I'm trying to do:
___PASTE_YOUR_CODE_OR_DESCRIBE___
```
</my-current-code>
**Questions:**
1. Which navigation method fits my scenario?
2. What are the tradeoffs of each option?
3. What happens if I use the wrong method?
Recommend the right navigation method with a code example for my specific scenario.Use v0 to scaffold a responsive nav bar and breadcrumb trail. Keep everything presentational; links must be real <Link> usages in our codebase, not hardcoded anchors from v0 output.
Prompt:
Generate a responsive top navigation with logo primary links and a breadcrumb component using Tailwind presentational only no data fetching output semantic HTML and accessible focus management.Open in v0: Open in v0
Try It
- Navigate between listing and detail; confirm no full page reload.
Commit & Deploy
git add -A
git commit -m "feat(core): implement soft navigation via next/link"
git push -u origin feat/core-next-linkDone-When
- Navigate to
/nav-demo, click "Go to Page A": URL changes to/nav-demo/page-awithout page refresh (soft navigation) - Click browser back button: returns to
/nav-demowith state preserved - Click "Hard nav to Page A": browser loading indicator appears (full page reload)
- On
/nav-demo, click "router.push()": navigates programmatically without page refresh - Click "router.replace()": navigates but back button skips the replaced entry
- Open DevTools Network tab during soft navigation: only RSC payload fetched (not full HTML)
Solution
Solution
This solution demonstrates soft navigation with next/link, programmatic navigation with useRouter, and server-side navigation with redirect().
Step 1: Create the Navigation Demo Page
// Lesson 2.6: Navigation Semantics
// Demonstrates soft navigation with next/link and useRouter
import Link from "next/link";
import { connection } from "next/server";
import { NavigationButtons } from "./navigation-buttons";
export default async function NavDemoPage() {
await connection(); // Opt out of prerendering - Date.now() needs request time
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-6 font-bold text-3xl">Navigation Demo</h1>
<div className="space-y-6">
{/* Link Component - Soft Navigation */}
<section className="rounded-lg border p-4">
<h2 className="mb-4 font-semibold text-xl">
Soft Navigation with Link
</h2>
<p className="mb-4 text-gray-600">
These links use next/link for client-side transitions. Notice how
the page does not fully reload.
</p>
<nav className="flex gap-4">
<Link
href="/nav-demo/page-a"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Go to Page A
</Link>
<Link
href="/nav-demo/page-b"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Go to Page B
</Link>
</nav>
</section>
{/* Hard Navigation Comparison */}
<section className="rounded-lg border p-4">
<h2 className="mb-4 font-semibold text-xl">
Hard Navigation (Avoid)
</h2>
<p className="mb-4 text-gray-600">
Plain anchor tags cause full page reloads. Notice the browser
refresh indicator.
</p>
<nav className="flex gap-4">
<a
href="/nav-demo/page-a"
className="rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"
>
Hard nav to Page A
</a>
</nav>
</section>
{/* Programmatic Navigation with useRouter */}
<section className="rounded-lg border p-4">
<h2 className="mb-4 font-semibold text-xl">
Programmatic Navigation
</h2>
<p className="mb-4 text-gray-600">
useRouter enables navigation from event handlers and conditional
logic.
</p>
<NavigationButtons />
</section>
{/* State Preservation Demo */}
<section className="rounded-lg border p-4">
<h2 className="mb-4 font-semibold text-xl">State Preservation</h2>
<p className="mb-4 text-gray-600">
Navigate to child pages and back. The layout state is preserved
during soft navigation.
</p>
<div className="rounded bg-gray-100 p-4">
<p className="text-gray-500 text-sm">
Current timestamp: {Date.now()}
</p>
<p className="text-gray-500 text-sm">
(This updates on hard navigation but not soft navigation from
child pages back)
</p>
</div>
</section>
</div>
{/* Key concepts summary */}
<div className="mt-8 rounded bg-gray-100 p-4">
<h3 className="mb-2 font-semibold">Key Concepts</h3>
<ul className="list-inside list-disc space-y-1 text-gray-600 text-sm">
<li>
<strong>Soft navigation:</strong> Client-side transitions, preserves
state
</li>
<li>
<strong>Hard navigation:</strong> Full page reload, clears state
</li>
<li>
<strong>Link component:</strong> Declarative soft navigation
</li>
<li>
<strong>useRouter:</strong> Programmatic client-side navigation
</li>
<li>
<strong>redirect():</strong> Server-side navigation (see below)
</li>
</ul>
</div>
</main>
);
}Step 2: Create Client Component for Programmatic Navigation
"use client";
// Client component for programmatic navigation with useRouter
// Must be a Client Component because useRouter uses React hooks
import { useRouter } from "next/navigation";
export function NavigationButtons() {
const router = useRouter();
return (
<div className="flex gap-4">
{/* push: adds to history stack */}
<button
type="button"
onClick={() => router.push("/nav-demo/page-a")}
className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
>
router.push()
</button>
{/* back: navigate to previous history entry */}
<button
type="button"
onClick={() => router.back()}
className="rounded border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50"
>
router.back()
</button>
{/* replace: replaces current history entry (no back button) */}
<button
type="button"
onClick={() => router.replace("/nav-demo/page-b")}
className="rounded bg-orange-600 px-4 py-2 text-white hover:bg-orange-700"
>
router.replace()
</button>
</div>
);
}Step 3: Create Child Pages for Navigation Testing
import Link from "next/link";
export default function PageA() {
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 font-bold text-2xl">Page A</h1>
<p className="mb-6 text-gray-600">
You navigated here via soft navigation. The layout was preserved.
</p>
<nav className="flex gap-4">
<Link
href="/nav-demo"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Back to Demo
</Link>
<Link
href="/nav-demo/page-b"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Go to Page B
</Link>
</nav>
</main>
);
}import Link from "next/link";
export default function PageB() {
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 font-bold text-2xl">Page B</h1>
<p className="mb-6 text-gray-600">
You navigated here via soft navigation. The layout was preserved.
</p>
<nav className="flex gap-4">
<Link
href="/nav-demo"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Back to Demo
</Link>
<Link
href="/nav-demo/page-a"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Go to Page A
</Link>
</nav>
</main>
);
}Step 4: Server-Side Navigation with redirect()
Use redirect() for server-side navigation, authentication checks, and after form submissions:
// Server-side redirect for authentication
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function ProtectedPage() {
const session = await getSession();
// Redirect unauthenticated users to login
if (!session) {
redirect("/login");
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 font-bold text-2xl">Protected Content</h1>
<p>Welcome, {session.user.name}!</p>
</main>
);
}We use /posts routes in apps/web to demonstrate navigation patterns. These demo routes are separate from the actual blog app (apps/blog) which has its own routing structure.
Step 5: Redirect After Server Action (Form Submission)
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Save to database
const post = await db.post.create({
data: { title, content },
});
// Revalidate the posts list so it shows the new post
revalidatePath("/posts");
// Redirect to the newly created post
// Server Actions use 303 status code by default
redirect(`/posts/${post.slug}`);
}import { createPost } from "./actions";
export default function NewPostPage() {
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-6 font-bold text-2xl">Create New Post</h1>
<form action={createPost} className="space-y-4">
<div>
<label htmlFor="title" className="block font-medium text-gray-700">
Title
</label>
<input
type="text"
id="title"
name="title"
required
className="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="content" className="block font-medium text-gray-700">
Content
</label>
<textarea
id="content"
name="content"
rows={5}
required
className="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
/>
</div>
<button
type="submit"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Create Post
</button>
</form>
</main>
);
}Step 6: Conditional Redirect Based on Data
import { redirect } from "next/navigation";
import { notFound } from "next/navigation";
async function fetchUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) return null;
return res.json();
}
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function UserPage({ params }: PageProps) {
const { id } = await params;
const user = await fetchUser(id);
// Redirect to login if user not found (auth scenario)
if (!user) {
redirect("/login");
}
// Alternative: show 404 for missing resources
// if (!user) {
// notFound();
// }
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 font-bold text-2xl">{user.name}</h1>
<p>{user.email}</p>
</main>
);
}Key Decisions Explained
-
Link over anchor tags:
<Link>provides soft navigation (client-side transitions) that preserves state, enables prefetching, and feels faster. Plain<a>tags trigger hard navigation with full page reloads. -
useRouter for programmatic navigation: When you need to navigate based on events or conditions (button clicks, form submissions, conditional logic), use
useRouterin a Client Component. -
redirect() for server-side navigation: Use
redirect()in Server Components, Server Actions, and Route Handlers. Common uses include authentication checks and redirecting after mutations. -
type="button" on all buttons: Biome requires explicit
typeattributes on buttons. Without it, buttons default totype="submit"which can cause unintended form submissions. -
router.replace vs router.push: Use
replacewhen you don't want users to navigate back (e.g., after login redirect). Usepushfor normal navigation where back button should work. -
revalidatePath before redirect: When redirecting after a mutation, call
revalidatePath()first to ensure the destination page shows fresh data.
References
Was this helpful?