Table of Contents
- What is React Router?
- Installation
- Core Concepts
- Dynamic Routing with URL Parameters
- Nested Routes
- Programmatic Navigation
- Route Protection (Guards)
- Advanced Features
- Best Practices
- Conclusion
- References
What is React Router?
React Router is a lightweight, flexible routing library for React that allows you to declaratively map URL paths to components. It leverages React’s component model to render different UI elements based on the current URL, enabling:
- Client-side navigation: No full page reloads—only the necessary components update.
- URL state management: The browser’s back/forward buttons work as expected.
- Dynamic routes: Handle variable data in URLs (e.g.,
/users/123). - Nested routes: Create complex UIs with shared layouts (e.g., dashboards with sidebars).
React Router is maintained by the Remix team (formerly React Training) and is compatible with React 16.8+ (hooks support). It has three primary packages:
react-router: Core routing logic (shared across platforms).react-router-dom: Web-specific bindings (we’ll focus on this).react-router-native: Mobile-specific bindings for React Native.
Installation
To get started with React Router in a web project, install react-router-dom (we’ll use v6, the latest stable version as of 2024):
# Using npm
npm install react-router-dom@6
# Using yarn
yarn add react-router-dom@6
Once installed, you’re ready to configure routing in your app.
Core Concepts
Let’s break down the foundational components and hooks of React Router v6.
BrowserRouter
Every React Router app starts with a router component that manages the routing state. For web apps, BrowserRouter is the most common choice—it uses the HTML5 history API (e.g., pushState, replaceState) to sync the URL with the app’s state.
Wrap your entire app (or the root component) with BrowserRouter to enable routing:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Routes and Route
The Routes component is a container for Route elements. It scans its children Route components and renders the first one whose path matches the current URL. Think of Routes as a “router switch” that picks the correct component to display.
A Route defines a mapping between a URL path and a component. It takes two key props:
path: The URL path to match (e.g.,/about).element: The React component to render when the path matches.
Example: Basic Routing
// src/App.js
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
function App() {
return (
<div className="App">
<Routes>
{/* Render Home when URL is "/" */}
<Route path="/" element={<Home />} />
{/* Render About when URL is "/about" */}
<Route path="/about" element={<About />} />
{/* Render Contact when URL is "/contact" */}
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
export default App;
path="/"is the default route (homepage).Routesensures only oneRouterenders at a time (unlike the oldSwitchcomponent, which it replaces).
Link
To navigate between routes without full page reloads, use the Link component instead of <a> tags. Link uses client-side navigation, updating the URL and re-rendering components efficiently.
Example: Navigation Menu
// src/components/Navbar.js
import { Link } from 'react-router-dom';
function Navbar() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
export default Navbar;
toprop: The target URL (relative or absolute).Linkrenders as an<a>tag in the DOM but prevents the default full-page reload.
Outlet
Outlet is used with nested routes to render child route elements. Think of it as a “placeholder” where child components will be injected.
For example, if you have a dashboard with nested routes (/dashboard, /dashboard/profile, /dashboard/settings), the parent Dashboard component can use Outlet to render Profile or Settings when their paths match.
We’ll dive deeper into nested routes in a later section, but here’s a sneak peek:
// src/pages/Dashboard.js
import { Outlet, Link } from 'react-router-dom';
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
</nav>
{/* Child routes (Profile/Settings) render here */}
<Outlet />
</div>
);
}
Dynamic Routing with URL Parameters
Often, you need to handle dynamic data in URLs (e.g., /users/123 where 123 is a user ID). React Router supports URL parameters for this, denoted by :paramName in the path prop.
Step 1: Define a Dynamic Route
// src/App.js
<Routes>
{/* Dynamic route: matches /users/1, /users/abc, etc. */}
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
Step 2: Access Parameters with useParams
Use the useParams hook to extract parameters from the URL:
// src/pages/UserProfile.js
import { useParams } from 'react-router-dom';
function UserProfile() {
// Extract userId from the URL
const { userId } = useParams();
return <h1>User Profile: {userId}</h1>;
}
export default UserProfile;
Now, navigating to /users/456 will render User Profile: 456.
Nested Routes
Nested routes are critical for building apps with shared layouts (e.g., dashboards, admin panels). They allow you to render components inside other components based on the URL hierarchy.
Example: Dashboard with Nested Routes
Let’s create a dashboard with:
- A parent route:
/dashboard(renders a layout with a sidebar). - Child routes:
/dashboard/profileand/dashboard/settings(render inside the parent layout).
Step 1: Define Nested Routes
// src/App.js
<Routes>
{/* Parent route with nested children */}
<Route path="/dashboard" element={<Dashboard />}>
{/* Child routes: rendered via Outlet in Dashboard */}
<Route index element={<DashboardHome />} /> {/* Default child ("/dashboard") */}
<Route path="profile" element={<Profile />} /> {/* "/dashboard/profile" */}
<Route path="settings" element={<Settings />} /> {/* "/dashboard/settings" */}
</Route>
</Routes>
indexprop: Marks the default child route (renders when the parent path matches but no child path is specified).
Step 2: Use Outlet in the Parent Component
The parent Dashboard component uses Outlet to render child routes:
// src/pages/Dashboard.js
import { Outlet, Link } from 'react-router-dom';
function Dashboard() {
return (
<div className="dashboard">
<aside>
<h2>Menu</h2>
<Link to="/dashboard">Home</Link>
<Link to="profile">Profile</Link> {/* Relative path (shorthand for "/dashboard/profile") */}
<Link to="settings">Settings</Link>
</aside>
<main>
{/* Child routes render here */}
<Outlet />
</main>
</div>
);
}
Now:
/dashboardrendersDashboardwithDashboardHomein theOutlet./dashboard/profilerendersDashboardwithProfilein theOutlet.
Programmatic Navigation
Sometimes you need to navigate after an action (e.g., form submission, login). Use the useNavigate hook to trigger navigation programmatically.
Basic Usage
import { useNavigate } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
const handleLogin = () => {
// Simulate login
const isAuthenticated = true;
if (isAuthenticated) {
// Navigate to dashboard
navigate('/dashboard');
}
};
return <button onClick={handleLogin}>Login</button>;
}
Key Features of useNavigate
- Replace history entry: Use
navigate('/dashboard', { replace: true })to replace the current URL in history (avoids “back” button returning to the login page). - Go back/forward:
navigate(-1)(back),navigate(1)(forward). - Pass state: Attach data to the navigation (e.g.,
navigate('/dashboard', { state: { from: '/login' } })).
Route Protection (Guards)
You’ll often need to restrict access to routes (e.g., requiring authentication). Create a route guard (e.g., PrivateRoute) to check if a user is authenticated before rendering the route.
Step 1: Create a PrivateRoute Component
// src/components/PrivateRoute.js
import { Navigate, Outlet } from 'react-router-dom';
// Mock auth check (replace with real auth logic)
const isAuthenticated = () => {
return localStorage.getItem('token') !== null;
};
function PrivateRoute() {
if (!isAuthenticated()) {
// Redirect to login if not authenticated
return <Navigate to="/login" replace />;
}
// Render child routes if authenticated
return <Outlet />;
}
export default PrivateRoute;
Step 2: Use PrivateRoute in Routes
Wrap protected routes with PrivateRoute:
// src/App.js
<Routes>
<Route path="/login" element={<Login />} />
{/* Protect dashboard routes */}
<Route element={<PrivateRoute />}>
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
</Route>
</Route>
</Routes>
Now, unauthenticated users trying to access /dashboard will be redirected to /login.
Advanced Features
useLocation
The useLocation hook returns the current location object, which contains info about the current URL (pathname, search, state, etc.). Useful for tracking page views or syncing state with the URL.
import { useLocation } from 'react-router-dom';
function PageTracker() {
const location = useLocation();
React.useEffect(() => {
// Log page view (e.g., for analytics)
console.log(`Visited: ${location.pathname}`);
}, [location.pathname]);
return null;
}
useMatch
useMatch checks if the current URL matches a given path pattern. Useful for active navigation links or conditional rendering.
import { useMatch } from 'react-router-dom';
function NavItem({ to, children }) {
const match = useMatch(to);
return (
<Link to={to} style={{ color: match ? 'blue' : 'black' }}>
{children}
</Link>
);
}
Query Parameters with useSearchParams
For query parameters (e.g., /search?query=react&page=1), use useSearchParams (similar to useState but for the URL search string).
// src/pages/Search.js
import { useSearchParams } from 'react-router-dom';
function Search() {
// [getter, setter] for query params
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('query');
const page = searchParams.get('page') || '1';
const handleSearch = (newQuery) => {
setSearchParams({ query: newQuery, page: '1' });
};
return (
<div>
<input
type="text"
placeholder="Search..."
value={query || ''}
onChange={(e) => handleSearch(e.target.value)}
/>
<p>Results for: {query}</p>
<p>Page: {page}</p>
</div>
);
}
Now, typing “react” in the input updates the URL to /search?query=react&page=1.
Best Practices
- Organize Routes Centrally: Store routes in a separate file (e.g.,
src/routes.js) for scalability. - Use Relative Paths: In nested routes, use relative
toprops (e.g.,to="profile"instead ofto="/dashboard/profile"). - Handle 404s: Add a catch-all route (
path="*") to render a “Not Found” page.<Route path="*" element={<NotFound />} /> - Avoid Overusing
useNavigate: PreferLinkfor user-triggered navigation; useuseNavigateonly for programmatic flows. - Test Routes: Use tools like
react-testing-libraryto test route rendering and navigation.
Conclusion
React Router is a powerful tool for building dynamic, navigable React apps. From basic page navigation to advanced features like nested routes and route guards, it provides everything you need to create a seamless user experience.
By mastering the concepts covered—core components, dynamic routing, nested routes, and route protection—you’ll be able to handle routing in projects of any scale. As you build more complex apps, explore React Router’s advanced hooks (e.g., useInRouterContext, useNavigation) and integrations with state management libraries like Redux.