Navigation in a React SPA is split between two concerns: letting users click their way around (declarative navigation with Link and NavLink), and sending users to a new page as a result of an action like form submission or successful login (programmatic navigation with useNavigate). Both are essential in the MERN Blog — the header uses NavLink to highlight the active page, post cards use Link to their detail page, and the login form uses useNavigate to redirect after authentication. In this lesson you will build all of these navigation patterns.
Link — Basic Client-Side Navigation
import { Link } from 'react-router-dom';
// Basic usage — replaces
<Link to="/">Home</Link>
<Link to="/posts/64a1f2b3c8e4d5f6a7b8c9d0">Read Post</Link>
<Link to="/register">Create Account</Link>
// Dynamic path using template literal
<Link to={`/posts/${post._id}`}>{post.title}</Link>
// With className for styling
<Link to="/posts/new" className="btn btn--primary">Write a Post</Link>
// Navigate to an external URL — use a plain tag
<a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub</a>
// Link only works for in-app navigation — never for external URLs
Link renders an <a> tag in the HTML but intercepts the click event and calls history.pushState() instead of following the href. This means all standard anchor behaviour works: keyboard navigation, right-click to open in new tab, hover to see the URL in the status bar. The difference is that a left-click triggers React Router navigation instead of a page reload.NavLink instead of Link for navigation items that should show an active state. NavLink automatically adds an active CSS class when the current URL matches its to prop — you can then style .active in CSS to highlight the current page in the nav bar, sidebar, or tab list.Link for external URLs — it only works for in-app routes. A <Link to="https://example.com"> would try to navigate to /https://example.com as a path within your app. For external links always use a standard <a> tag with target="_blank" and rel="noopener noreferrer".NavLink — Active-State Navigation
import { NavLink } from 'react-router-dom';
// ── Basic NavLink — auto-adds 'active' class when URL matches ──────────────────
function SiteNav() {
return (
<nav className="site-nav">
<NavLink to="/" end>Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
<NavLink to="/posts/new">New Post</NavLink>
</nav>
);
}
// When at /dashboard: <a class="active" href="/dashboard">Dashboard</a>
// The 'end' prop on Home prevents it matching when at /dashboard
// ── Custom active styling with className function ──────────────────────────────
<NavLink
to="/dashboard"
className={({ isActive }) => isActive ? 'nav-link nav-link--active' : 'nav-link'}
>
Dashboard
</NavLink>
// ── Custom active styling with style function ──────────────────────────────────
<NavLink
to="/posts/new"
style={({ isActive }) => ({
color: isActive ? '#3b82f6' : '#374151',
fontWeight: isActive ? '600' : '400',
})}
>
Write Post
</NavLink>
// ── CSS for the auto-added active class ───────────────────────────────────────
// index.css or App.css:
// .site-nav a.active {
// color: #3b82f6;
// border-bottom: 2px solid currentColor;
// }
useNavigate — Programmatic Navigation
import { useNavigate } from 'react-router-dom';
// ── Redirect after login ───────────────────────────────────────────────────────
function LoginPage() {
const navigate = useNavigate();
const [formData, setFormData] = useState({ email: '', password: '' });
const handleSubmit = async (e) => {
e.preventDefault();
try {
await authService.login(formData);
navigate('/dashboard'); // redirect on success
} catch (err) {
setError(err.message);
}
};
return <form onSubmit={handleSubmit}>...</form>;
}
// ── Go back ───────────────────────────────────────────────────────────────────
function EditPostPage() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate(-1)}>← Cancel</button> {/* go back */}
</div>
);
}
// ── Navigate with replace — removes current entry from history ─────────────────
// Useful after login so the user cannot go "back" to the login page
navigate('/dashboard', { replace: true });
// ── Navigate with state — pass data to the destination page ───────────────────
navigate('/login', { state: { from: location.pathname } });
// Then in LoginPage: const location = useLocation(); location.state?.from
The Complete Header Component
// src/components/layout/Header.jsx
import { Link, NavLink } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext'; // covered in Chapter 21
function Header() {
const { user, logout } = useAuth();
return (
<header className="site-header">
<div className="site-header__inner">
{/* Logo — Link takes user to home */}
<Link to="/" className="site-header__logo">
MERN Blog
</Link>
{/* Navigation */}
<nav className="site-header__nav">
<NavLink to="/" end>Home</NavLink>
{user ? (
{/* Authenticated nav */}
<>
<NavLink to="/dashboard">Dashboard</NavLink>
<NavLink to="/posts/new">Write Post</NavLink>
<button onClick={logout} className="btn btn--ghost">Log Out</button>
</>
) : (
{/* Guest nav */}
<>
<NavLink to="/login">Login</NavLink>
<NavLink to="/register" className="btn btn--primary">Register</NavLink>
</>
)}
</nav>
</div>
</header>
);
}
export default Header;
Common Mistakes
Mistake 1 — Calling navigate() during render
❌ Wrong — calling navigate during component rendering causes an infinite loop:
function SomePage() {
const navigate = useNavigate();
if (someCondition) navigate('/other'); // called during render — bad!
return <div>...</div>;
}
✅ Correct — use Navigate component for render-time redirects, or navigate inside effects/event handlers:
if (someCondition) return <Navigate to="/other" replace />; // ✓ render-time redirect
// Or inside a handler:
const handleClick = () => navigate('/other'); // ✓ event handler
Mistake 2 — Not adding the `end` prop to the root NavLink
❌ Wrong — Home link always shows as active:
<NavLink to="/">Home</NavLink>
// Matches "/" AND "/dashboard" AND "/posts/123" — always active!
✅ Correct — use end so it only matches exactly /:
<NavLink to="/" end>Home</NavLink> // ✓ only active when path is exactly /
Mistake 3 — Using navigate() before the component mounts
❌ Wrong — navigate called during render or before hooks are ready:
const navigate = useNavigate();
navigate('/login'); // called immediately on every render — infinite loop
✅ Correct — navigate inside effects or event handlers, never directly in the render body.
Quick Reference
| Task | Code |
|---|---|
| In-app link | <Link to="/path">Label</Link> |
| Dynamic path | <Link to={`/posts/${id}`}>Read</Link> |
| Nav with active class | <NavLink to="/dashboard">Dashboard</NavLink> |
| Root nav (exact) | <NavLink to="/" end>Home</NavLink> |
| Custom active class | className={({ isActive }) => isActive ? 'active' : ''} |
| Redirect after action | navigate('/dashboard') |
| Replace history entry | navigate('/dashboard', { replace: true }) |
| Go back | navigate(-1) |
| Render-time redirect | <Navigate to="/login" replace /> |