React Router
React Router
1. What is React Router?
React Router is the standard declarative routing library for React applications. It keeps your UI in sync with the browser URL, enabling navigation between views without a full page reload, the cornerstone of Single Page Applications (SPAs).
React is a UI library. It knows how to render components, manage state, and handle events, but it has no built-in mechanism to map a URL like /about to a specific component. React Router fills that gap.
Maintained by Remix (now part of Shopify), React Router is the most widely used routing solution in the React ecosystem with hundreds of millions of downloads per month.
Key idea: React Router doesn’t navigate to new HTML pages. It intercepts URL changes and renders the appropriate React components, giving the illusion of multi-page navigation with the speed of a SPA.
2. Why Do We Need a Router?
Without a router, React renders a single page at a fixed URL. Problems arise immediately:
- Refreshing the page always shows the same view
- The browser Back/Forward buttons do nothing meaningful
- You can’t share a link to a specific section like
/dashboard/settings - Bookmarks always go back to the root
React Router solves all of this by wiring React’s component tree to the browser’s URL, history stack, and navigation APIs.
3. Installation & Setup
terminal
npm install react-router-dom
Then wrap your entire app in a router provider. The most common choice for web apps is BrowserRouter:
main.jsx
import { createRoot } from 'react-dom/client';import { BrowserRouter } from 'react-router-dom';import App from './App';createRoot(document.getElementById('root')).render( <BrowserRouter> <App /> </BrowserRouter> );
App.jsx — basic route setup
import { Routes, Route, Link } from 'react-router-dom';import Home from './pages/Home';import About from './pages/About';import Profile from './pages/Profile';export default function App(){return (<><nav><Link to="/">Home</Link> <Link to="/about">About</Link><Link to="/profile/42">Profile</Link></nav><Routes> <Route path="/" element={<Home />} /><Route path="/about" element={<About />} /><Route path="/profile/:id" element={<Profile />} /> </Routes> </> ); }
4. Core Concepts
Routes & Route
<Routes> is the container that looks at the current URL and picks the first <Route> whose path matches. Only one child route renders at a time (unless you use nested routes).
Route patterns
<Routes>
{/* Exact match */}
<Route path="/" element={<Home />} />
{/* URL parameter: :id is dynamic */}
<Route path="/users/:id" element={<User />} />
{/* Wildcard: matches any remaining segments */}
<Route path="/docs/*" element={<Docs />} />
{/* Catch-all 404 page */}
<Route path="*" element={<NotFound />} />
</Routes>
Nested Routes
Nested routes let child components render inside parent layouts without re-mounting the entire tree. The parent renders <Outlet /> as the placeholder for the child.
Nested routes with Outlet
// Route config
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
// DashboardLayout.jsx
import { Outlet, NavLink } from 'react-router-dom';
export default function DashboardLayout() {
return (
<div className="layout">
<aside>
<NavLink to="">Home</NavLink>
<NavLink to="analytics">Analytics</NavLink>
<NavLink to="settings">Settings</NavLink>
</aside>
<main>
<Outlet /> {/* child route renders here */}
</main>
</div>
);
}
Link vs NavLink
<Link> is the basic navigation element — renders an <a> tag that intercepts the click and calls the history API instead of reloading the page. <NavLink> extends it with an automatic active class when the URL matches the to prop — perfect for navigation menus.
NavLink with active styling
<NavLink
to="/about"
className={({ isActive, isPending }) =>
isActive ? 'nav-link active' : 'nav-link'
}
style={({ isActive }) => ({
color: isActive ? '#7f8ef7' : 'inherit'
})}
>
About
</NavLink>
Navigate & Redirect
Programmatic redirect with Navigate component
import { Navigate } from 'react-router-dom';
function ProtectedPage({ isLoggedIn }) {
if (!isLoggedIn) {
// Redirect to /login, replacing history entry
return <Navigate to="/login" replace />;
}
return <div>Secret content</div>;
}
Search Params (Query Strings)
Reading and setting ?query=react
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
return (
<input
value={query}
onChange={e => setSearchParams({ q: e.target.value })}
placeholder="Search..."
/>
);
// URL becomes: /search?q=react-router
}
6. Built-in Hooks
React Router provides a set of hooks to access routing state and history from any component in the tree, without prop drilling.
| Hook | Returns | Use case |
|---|---|---|
| useNavigate() | navigate fn | Programmatic navigation after events (form submit, auth, etc.) |
| useParams() | { id, … } | Read URL parameters like /users/:id |
| useLocation() | location obj | Access pathname, search, hash, and state |
| useSearchParams() | [params, setter] | Read and write query string parameters |
| useMatch() | match | null | Test if current URL matches a given pattern |
| useLoaderData() | loader result | Access data returned from a route loader (v6.4+) |
| useActionData() | action result | Access result from a form action (v6.4+) |
| useNavigation() | navigation obj | Track loading state during navigation (v6.4+) |
| useRouteError() | error | Access the error inside an errorElement (v6.4+) |
| useOutlet() | outlet element | Programmatically access the child route element |
useNavigate — examples
useNavigate usage
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
await login();
navigate('/dashboard'); // go to /dashboard
navigate(-1); // go back (like browser back)
navigate('/login', { replace: true }); // replace history entry
navigate('/checkout', { // pass state
state: { from: '/cart' }
});
}
return <form onSubmit={handleSubmit}>...</form>;
}
useParams: example
Reading /products/:category/:id
import { useParams } from 'react-router-dom';function ProductDetail() {const { category, id } = useParams();// URL: /products/electronics/42// category = "electronics", id = "42"return <p>{category} — #{id}</p>; }
useLocation: example
Accessing location object
import { useLocation } from 'react-router-dom';
function Analytics() {
const location = useLocation();
// {
// pathname: '/about',
// search: '?ref=newsletter',
// hash: '#team',
// state: { from: '/home' },
// key: 'abc123'
// }
React.useEffect(() => {
trackPageView(location.pathname);
}, [location]);
}
7. Advanced Patterns
Protected Routes (Auth Guard)
The most common pattern, redirect unauthenticated users before a protected page mounts:
ProtectedRoute wrapper component
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './hooks/useAuth';
function ProtectedRoute() {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// Save where user was trying to go
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
// Usage in routes:
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
Lazy Loading Routes
Code-split large pages with React.lazy and Suspense — the route’s JS is only fetched when needed:
Lazy loading with Suspense
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Route-Level Error Boundaries (v6.4+)
Error element with useRouteError
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return <p>Something went wrong: {error.message}</p>;
}
Scroll Restoration
React Router v6.4+ handles scroll restoration automatically when using createBrowserRouter. For older setups, a simple hook:
useScrollToTop hook
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => window.scrollTo(0, 0), [pathname]);
return null;
}
// Place inside BrowserRouter, before Routes
<BrowserRouter>
<ScrollToTop />
<App />
</BrowserRouter>
8. What Changed in v6?
React Router v6 was a major overhaul. If you’re upgrading from v5, here’s what changed:
Removed in v6
Switch → Routes
<Switch> is gone. Use <Routes>. It’s smarter, it picks the best match, not just the first one.
New in v6
element prop
Routes use element={<Component />} instead of component={Component} or render={}. Simpler and type-safe.
New in v6
Outlet
Nested layouts use <Outlet /> instead of rendering children manually. Much cleaner layout composition.
Removed in v6
useHistory → useNavigate
useHistory() is gone. The replacement useNavigate() returns a function directly — cleaner API.
New in v6.4+
Data APIs
Loaders, actions, deferred data, and RouterProvider bring Remix-style data fetching into client React apps.
Changed in v6
No exact prop
All <Route path="/"> matches are exact by default. The old exact prop is gone.
9. Frequently Asked Questions
What’s the difference between BrowserRouter and HashRouter?
/about) via the HTML5 History API, which requires your server to return index.html for every route. HashRouter uses the hash fragment (/#/about), which never leaves the browser, so it works without any server configuration. Use HashRouter for static hosting (GitHub Pages, S3 without CloudFront), BrowserRouter everywhere else.When should I use RouterProvider vs BrowserRouter?
createBrowserRouter + RouterProvider. For simpler apps that don’t need those features, BrowserRouter is perfectly fine and has a simpler mental model.How do I pass state between routes?
state prop on <Link> or navigate(): navigate('/checkout', { state: { items } }). Read it on the destination with useLocation().state. Note: state lives in session history — it’s gone after a hard refresh. For persistent data, use URL params or a global store.How do I handle 404 pages?
<Routes>: <Route path="*" element={<NotFound />} />. The * wildcard matches anything that no other route matched. It must be last because React Router picks the best match, but a wildcard always matches.Can I use React Router with TypeScript?
react-router-dom ships with TypeScript types built-in (no @types/ package needed). Hook return types like useParams<{ id: string }>() give you typed route params. The data router API also supports generic typing for loaders and actions.<Route index element={...} />) is the default child rendered when the parent’s URL matches exactly and no other child route matches. For example, at /dashboard, the index route renders instead of nothing.Is React Router compatible with React 18?
Suspense. The data router’s loaders integrate cleanly with React.lazy and Suspense fallbacks.How do I animate route transitions?
AnimatePresence. Wrap your <Routes> with <AnimatePresence> and use useLocation() as the key prop so Framer Motion detects route changes. Each page component wraps its content in a <motion.div> with initial, animate, and exit props.What’s the difference between Link and a regular <a> tag?
<a href="..."> triggers a full browser navigation — the server is hit and the entire React app re-initialises. <Link to="..."> intercepts the click event, updates the URL via the History API, and re-renders only the matching components. This is orders of magnitude faster and preserves all React state.How do I test components that use React Router?
<MemoryRouter initialEntries={['/your/path']}>. Use initialEntries to set the starting URL. If your component uses hooks like useParams, make sure the path pattern matches by also providing a <Route path="..."> inside the MemoryRouter.Sources:
