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;Click Run to see what this code prints.
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> );}Click Run to see what this code prints.
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
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.
<NavLink> is <Link> plus automatic awareness of whether it matches the current URL, which you can use to style the active link differently.
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.
Yes. Calling navigate(-1) goes back one entry in the browser history, similar to clicking the browser's back button.
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.