Navigation — Link, NavLink and the useNavigate Hook

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.

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
Note: 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.
Tip: Use 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.
Warning: Never use React Router’s 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".
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

❌ 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 />

🧠 Test Yourself

You add <NavLink to="/">Home</NavLink> to the header. When the user visits /dashboard, the Home link shows as active. Why and how do you fix it?