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.
- 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.
The <Link> Component
<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.
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;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.
The <NavLink> Component
<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.
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;<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.
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.
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>
);
}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.
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;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
- 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.
- 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.