LearnContact
Lesson 4222 min read

Navigation

Learn how to navigate between routes with <Link> and <NavLink>, trigger navigation programmatically with useNavigate(), and build a catch-all 404 page.

Introduction

Defining routes is only half the story — users also need a way to move between them. A plain HTML <a> tag would reload the entire page from the server, throwing away all of React's in-memory state and defeating the purpose of a single-page application.

React Router provides its own navigation tools that update the URL and swap the matched route without a full page reload: the <Link> and <NavLink> components for user-clickable navigation, and the useNavigate() hook for navigating from code. This lesson covers both, plus how to catch unmatched URLs with a 404 page.

What You Will Learn
  • How <Link> replaces <a> tags for client-side navigation.
  • How <NavLink> automatically styles the currently active link.
  • How to navigate programmatically with the useNavigate() hook.
  • A worked example of redirecting a user after a successful login.
  • How to build a catch-all route for a 404 Not Found page.

<Link> renders an accessible <a> element under the hood, but intercepts the click so React Router can update the URL and swap routes without asking the browser to reload the page.

Nav.jsx
import { Link } from 'react-router-dom';

function Nav() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
    </nav>
  );
}

export default Nav;
Never Use <a href> for Internal Routes

A plain <a href="/about"> forces a full browser page reload, resetting all component state. Always use <Link to="..."> for routes inside your own app.

<NavLink> behaves exactly like <Link>, but it also knows whether it matches the current URL. It automatically adds an "active" class (or lets you compute styles/classes yourself) to whichever link corresponds to the page the user is currently viewing — perfect for highlighting the current tab in a navbar.

NavBar.jsx
import { NavLink } from 'react-router-dom';

function NavBar() {
  return (
    <nav>
      <NavLink
        to="/"
        end
        className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
      >
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
      >
        About
      </NavLink>
    </nav>
  );
}

export default NavBar;
Rendered While on /about
<a class="nav-link">Home</a>
<a class="nav-link active">About</a>

Notes on NavLink

  • The "isActive" boolean is passed to the className (or style) function automatically by React Router.
  • The "end" prop makes a link only match exactly, so "/" is not treated as active while on "/about".
  • Without "end", a link to "/" would stay highlighted on every nested route, since "/" is a prefix of every path.

Programmatic Navigation with useNavigate

Sometimes navigation should not wait for a click — for example, redirecting a user to a dashboard right after a form submits successfully. The useNavigate() hook returns a function you can call from anywhere in your component logic to change routes.

useNavigate Basics
import { useNavigate } from 'react-router-dom';

function Example() {
  const navigate = useNavigate();

  function goToDashboard() {
    navigate('/dashboard');
  }

  return <button onClick={goToDashboard}>Go to Dashboard</button>;
}

Example: Redirect After Login

A common real-world use case is redirecting the user only after an async operation succeeds, such as a login request. useNavigate() lets you call navigate() inside the success branch of that logic.

LoginForm.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    const success = await fakeLogin(email, password);

    if (success) {
      navigate('/dashboard');
    } else {
      setError('Invalid email or password.');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
      <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="Password" />
      {error && <p className="error">{error}</p>}
      <button type="submit">Log In</button>
    </form>
  );
}
After a Successful Login
URL changes from /login to /dashboard
The DashboardLayout route renders automatically.

Catch-All Not Found Route

When a user visits a URL that does not match any defined route — a typo, an old bookmark, a broken link — you want to show a friendly 404 page instead of a blank screen. React Router supports this with a wildcard path of "*", which matches any URL that no earlier route matched.

App.jsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

export default App;
Order Matters

Keep the "*" route last. React Router matches routes in the order that makes the best match, but conventionally the catch-all is placed at the end since it should only apply when nothing more specific matched.

Common Mistakes

Avoid These Mistakes
  • Using <a href="..."> instead of <Link to="..."> for internal navigation, causing a full page reload.
  • Forgetting the "end" prop on a <NavLink to="/">, which keeps it visually "active" on every route.
  • Calling navigate() before an async action actually succeeds, redirecting users even on failed logins.
  • Forgetting to add a "*" catch-all route, leaving users with a blank screen on an unmatched URL.

Best Practices

  • Always use <Link> or <NavLink> instead of raw <a> tags for routes inside your app.
  • Use <NavLink> anywhere you need to visually highlight the current page, like a navbar or sidebar.
  • Reserve useNavigate() for navigation triggered by logic — form submissions, timers, or redirects — not simple clickable links.
  • Place your catch-all "*" route at the end of your route list so it only matches truly unmatched URLs.
  • Give your 404 page a link back to the home page so users are not stuck.

Frequently Asked Questions

Why not just use <a> tags everywhere?

A plain <a> tag triggers a full browser page reload, which throws away all of your React app's in-memory state and is much slower than a client-side route change.

What is the difference between <Link> and <NavLink>?

<NavLink> is <Link> plus automatic awareness of whether it matches the current URL, which you can use to style the active link differently.

When should I use useNavigate instead of <Link>?

Use useNavigate() when navigation should happen as a result of code running, such as after a successful form submission, rather than a direct user click on a link.

Can useNavigate go back in history?

Yes. Calling navigate(-1) goes back one entry in the browser history, similar to clicking the browser's back button.

Does the catch-all route need to be named NotFound?

No, you can name the component anything. What matters is the path="*", which matches any URL not caught by an earlier, more specific route.

Key Takeaways

  • <Link> replaces <a> tags for internal navigation without full page reloads.
  • <NavLink> automatically knows if it matches the current route, letting you style the active link.
  • useNavigate() returns a function for triggering navigation from code, such as after an async action succeeds.
  • A route with path="*" acts as a catch-all for unmatched URLs, ideal for a 404 Not Found page.
  • Place catch-all routes last so more specific routes are matched first.

Summary

Navigation is what turns a set of defined routes into an actual usable application. <Link> and <NavLink> handle click-driven navigation, while useNavigate() gives you full control to redirect users from within your own logic.

In this lesson, you learned how <Link> and <NavLink> work, how to navigate programmatically with useNavigate(), and how to build a catch-all 404 route. Next, you will learn how to lazy load route components so users only download the code they actually need.

Lesson 42 Completed
  • You can navigate between routes using <Link>.
  • You can highlight the active route using <NavLink>.
  • You can trigger navigation from code using useNavigate().
  • You can build a catch-all 404 page with a "*" route.
Next Lesson →

Lazy Loading