π Navigation and Links
Routes decide what renders; navigation decides how users get there. Good navigation feels invisible β links that highlight the current page, buttons that redirect after a save, a back gesture that lands exactly where it should. This lesson turns React Router's navigation tools into muscle memory.
Week 5 · Day 2 (Tuesday: React Router) · Lecture 3
π― Learning Objectives
By the end of this lesson, you will be able to:
- Use
<Link>for declarative navigation and passstatewithout exposing it in the URL - Style the current page with
<NavLink>'sisActiveand theendprop - Navigate programmatically with
useNavigate, includingreplaceand relative moves - Read the current location and query string with
useLocationanduseSearchParams - Build reusable navigation patterns: breadcrumbs, tabs, and post-action redirects
- Make navigation accessible with semantic markup and
aria-current
Estimated Time: 55 minutes
Practice: Build an accessible tab bar and a search box that stores its query in the URL.
In This Lesson
Navigation, the big picture
Think of navigation in a React app like a good GPS. It doesn't just move you between destinations β it remembers where you've been (the history stack), lets you retrace your route (back/forward), and can quietly reroute you when needed (a redirect after login). React Router gives you two families of tools to build that experience.
The rule of thumb never changes: if a human clicks to go somewhere, use a Link; if your code decides to go somewhere, use useNavigate. Everything below is a variation on those two.
The Link component
<Link> renders a real <a> tag β so it's keyboard-focusable, right-clickable, and openable in a new tab β but it intercepts the click to navigate without a reload. The to prop is all you need for the common case.
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* Simplest form */}
<Link to="/">Home</Link>
{/* to can be an object: path + query + hash */}
<Link to={{ pathname: '/products', search: '?sort=popular', hash: '#featured' }}>
Popular products
</Link>
{/* Pass data WITHOUT putting it in the URL */}
<Link to="/dashboard" state={{ from: 'nav' }}>Dashboard</Link>
{/* Replace the current history entry instead of pushing a new one */}
<Link to="/login" replace>Login</Link>
</nav>
);
}
π‘ What is state?
The state prop attaches data to a navigation that travels with the history entry but never appears in the URL β perfect for "you came from the cart" flags or a scroll position. Read it on the other side with useLocation().state. Because it's not in the URL, it's gone on a hard refresh, so treat it as transient.
Reading location & query
Two hooks let you inspect where you are.
useLocation β the current address
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
function LocationAware() {
const location = useLocation();
// location.pathname β "/users/123"
// location.search β "?tab=profile"
// location.hash β "#section1"
// location.state β { from: "/dashboard" } (set via Link/navigate)
// A classic use: log a page view whenever the URL changes
useEffect(() => {
analytics.pageView(location.pathname + location.search);
}, [location]);
return <p>You are at {location.pathname}</p>;
}
useSearchParams β read & write the query string
Query strings are perfect for state you want to be shareable and bookmarkable β a search term, a filter, a page number. useSearchParams works like useState, but the "state" lives in the URL.
import { useSearchParams } from 'react-router-dom';
function ProductSearch() {
const [searchParams, setSearchParams] = useSearchParams();
// Read (with sensible defaults)
const query = searchParams.get('q') ?? '';
const page = Number(searchParams.get('page') ?? '1');
// Write β build a fresh URLSearchParams so React re-renders
const updateQuery = (newQuery) => {
const params = new URLSearchParams(searchParams);
params.set('q', newQuery);
params.set('page', '1'); // reset to first page on a new search
setSearchParams(params);
};
return (
<div>
<input
value={query}
onChange={(e) => updateQuery(e.target.value)}
placeholder="Search productsβ¦"
/>
<p>Searching “{query}” β page {page}</p>
</div>
);
}
π‘ Why store state in the URL?
Put a filter in useState and refreshing loses it; share the link and your friend sees the default view. Put it in the query string and the URL is the state β refresh-proof, bookmarkable, and shareable. That's a big usability win for search and filter UIs.
Common patterns
A few navigation recipes come up in nearly every app. Here are three you can lift into your own projects.
1. Breadcrumbs from the path
import { useLocation, Link } from 'react-router-dom';
function Breadcrumbs() {
const { pathname } = useLocation();
const parts = pathname.split('/').filter(Boolean); // ["users","42","settings"]
return (
<nav aria-label="Breadcrumb">
<ol>
<li><Link to="/">Home</Link></li>
{parts.map((part, i) => {
const to = '/' + parts.slice(0, i + 1).join('/');
const isLast = i === parts.length - 1;
return (
<li key={to}>
{isLast ? <span aria-current="page">{part}</span>
: <Link to={to}>{part}</Link>}
</li>
);
})}
</ol>
</nav>
);
}
2. A tab bar with NavLink
function DashboardTabs() {
const tabs = [
{ to: '/dashboard/overview', label: 'Overview' },
{ to: '/dashboard/analytics', label: 'Analytics' },
{ to: '/dashboard/reports', label: 'Reports' },
];
return (
<div className="tabs" role="tablist">
{tabs.map(({ to, label }) => (
<NavLink key={to} to={to} role="tab"
className={({ isActive }) => `tab ${isActive ? 'active' : ''}`}>
{label}
</NavLink>
))}
</div>
);
}
3. Redirect back to where you came from
function Login() {
const navigate = useNavigate();
const location = useLocation();
// A protected route can send you here with state.from set
const from = location.state?.from ?? '/dashboard';
const handleLogin = async () => {
await logIn();
navigate(from, { replace: true }); // return to the page they wanted
};
return <button onClick={handleLogin}>Log in</button>;
}
Accessible navigation
Navigation is one of the highest-impact places to get accessibility right β it's how everyone, including keyboard and screen-reader users, moves through your app. React Router helps, but a few habits seal the deal.
function AccessibleNav() {
return (
// A real <nav> with a label announces its purpose
<nav aria-label="Main navigation">
<ul>
<li>
{/* NavLink auto-adds aria-current="page" when active */}
<NavLink to="/" end>Home</NavLink>
</li>
<li><NavLink to="/about">About</NavLink></li>
<li><NavLink to="/contact">Contact</NavLink></li>
</ul>
</nav>
);
}
β
NavLink gives you aria-current free
When a NavLink is active it automatically sets aria-current="page", which screen readers announce as "current page." That's a real accessibility win you'd otherwise have to wire by hand. Combine it with a genuine <nav aria-label> and semantic list markup and your navigation is usable by everyone.
- Wrap primary navigation in a
<nav>with a descriptivearia-label - Use a real skip-to-content link (like the one at the top of this page) so keyboard users can bypass the menu
- Never build a "link" out of a
<div onClick>β<Link>renders a focusable anchor for you
Practice & Quiz
ποΈ Exercise 1: An accessible tab bar
Goal: Build a tab bar from an array of { to, label } objects where the current tab is visually active and announced to screen readers.
const tabs = [
{ to: '/settings/profile', label: 'Profile' },
{ to: '/settings/billing', label: 'Billing' },
];
// TODO: render a NavLink per tab with an active class
π‘ Hint
Map over tabs and render a <NavLink> for each. The className callback receives { isActive }. NavLink already sets aria-current="page" when active, so screen-reader support comes for free.
β Solution
import { NavLink } from 'react-router-dom';
function SettingsTabs({ tabs }) {
return (
<nav aria-label="Settings" className="tabs">
{tabs.map(({ to, label }) => (
<NavLink key={to} to={to}
className={({ isActive }) => `tab ${isActive ? 'active' : ''}`}>
{label}
</NavLink>
))}
</nav>
);
}
ποΈ Exercise 2: Search state in the URL
Goal: Make a search input whose value is stored in the query string as ?q=β¦, so refreshing or sharing the URL preserves the search.
β Solution
import { useSearchParams } from 'react-router-dom';
function Search() {
const [params, setParams] = useSearchParams();
const q = params.get('q') ?? '';
return (
<input
value={q}
onChange={(e) => {
const next = new URLSearchParams(params);
e.target.value ? next.set('q', e.target.value) : next.delete('q');
setParams(next);
}}
placeholder="Searchβ¦"
/>
);
}
π― Quick Quiz
Question 1: You need to redirect the user after a successful form submit. Which tool fits?
Question 2: Why add the end prop to a <NavLink to="/">?
Question 3: Where should a shareable, bookmarkable search term live?
Best Practices & Pitfalls
β Do
- Use
<Link>/<NavLink>for user clicks; reserveuseNavigatefor logic-driven redirects - Add
endto anyNavLinkwhose path is a prefix of others (especially"/") - Store shareable UI state (search, filters, page) in the query string with
useSearchParams - Use
{ replace: true }after logins and redirects so Back doesn't return to a dead end - Keep navigation in semantic
<nav>+ list markup and letNavLinksupplyaria-current
β Don't
- Build clickable navigation from
<div onClick>β it's inaccessible and un-focusable - Put sensitive data in the URL or in
stateexpecting it to survive a refresh (state won't) - Mutate
searchParamsin place β build a newURLSearchParamsso React re-renders - Reach for
useNavigatewhere a plain<Link>would do β you'd lose right-click and new-tab support
β οΈ Hooks only run inside the router
useNavigate, useLocation, and friends must be called from components rendered inside your router (below <RouterProvider> or <BrowserRouter>). Call one outside and you'll get "useNavigate() may be used only in the context of a <Router>."
Summary
π Key Takeaways
- Clicks β
<Link>/<NavLink>; code βuseNavigateis the rule that guides every decision <NavLink>'sisActive(plusend) styles the current page and setsaria-currentfor freeuseNavigatesupportsreplaceand relative moves likenavigate(-1)useLocationreads the current address;useSearchParamsmakes the URL the source of truth for search & filters- Breadcrumbs, tabs, and post-action redirects are small, reusable compositions of these tools
- Semantic
<nav>markup + real anchors from<Link>keep navigation accessible
π Additional Resources
- React Router β Navigating (Link, NavLink, useNavigate)
- React Router β useSearchParams
- MDN β The navigation ARIA role
π What's Next?
You can now move users anywhere and highlight where they are. Next we compose routes inside routes: the Nested Routes lesson builds multi-level layouts where a parent screen keeps its shell while child routes swap through a shared <Outlet />.
π Navigation mastered!
Links that highlight, redirects that land right, and URLs that remember β your app now feels like a real product.