React Router — Declarative Routing for React Single-Page Applications
In this tutorial, you will learn about React Router. We cover key concepts, practical examples, and best practices to help you master this topic.
React Router provides declarative, component-based routing for React SPAs with nested routes, URL parameters, Lazy Loading, and navigation guards for production applications.
What You'll Learn
By the end of this tutorial, you will understand how to set up React Router, define routes with parameters, handle navigation, implement nested layouts, and protect routes with authentication guards.
Why It Matters
React Router is the de facto standard routing library for React applications with over 10 million weekly npm downloads. Mastering it is essential for building any non-trivial React SPA that needs multiple views, URL-based navigation, and deep linking.
Real-World Use
A dashboard SPA uses React Router v6 with nested routes: a main layout containing a sidebar, header, and content area. The routes /dashboard, /dashboard/users, and /dashboard/settings all render within the layout. URL parameters like /dashboard/users/123 load specific user profiles.
React Router Setup
React Router Architecture
BrowserRouter (listens to URL changes)
↓
Routes (matches URL to route definition)
↓
Route (renders component when path matches)
↓
Layout Component (shared UI: nav, sidebar)
↓
Outlet (renders child route content)
↓
Link / NavLink (client-side navigation)
Think of React Router like a GPS navigation system for your app. You define possible routes (destinations), and when the URL changes (you move), React Router guides the rendering (shows the right view). Nested routes are like navigating into sub-regions.
Basic Setup
// App.js
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function Home() {
return <h1>Home Page</h1>;
}
function About() {
return <h1>About Page</h1>;
}
function Contact() {
return <h1>Contact Page</h1>;
}
function NotFound() {
return <h1>404 — Page Not Found</h1>;
}
Nested Routes with Layouts
import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom';
// Layout component wraps child routes
function DashboardLayout() {
return (
<div className="dashboard">
<aside className="sidebar">
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/users">Users</Link>
<Link to="/dashboard/settings">Settings</Link>
</aside>
<main className="content">
<Outlet /> {/* Child route renders here */}
</main>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
<Route path="users" element={<UsersList />} />
<Route path="users/:userId" element={<UserDetail />} />
<Route path="settings" element={<Settings />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Route Parameters
import { useParams, useSearchParams } from 'react-router-dom';
// URL parameter: /users/123
function UserDetail() {
const { userId } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <div>Loading user {userId}...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// Query parameters: /search?q=react&page=2
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const page = parseInt(searchParams.get('page') || '1');
function updateQuery(newQuery) {
setSearchParams({ q: newQuery, page: '1' });
}
return (
<div>
<input
value={query}
onChange={(e) => updateQuery(e.target.value)}
placeholder="Search..."
/>
<p>Search results for "{query}", page {page}</p>
</div>
);
}
Programmatic Navigation
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(event) {
event.preventDefault();
try {
await login(event.target.email.value, event.target.password.value);
// Navigate to dashboard after login
navigate('/dashboard', { replace: true });
} catch (error) {
// Stay on login, show error
setError('Invalid credentials');
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
<p>
<a href="/register" onClick={(e) => {
e.preventDefault();
navigate('/register');
}}>
Create account
</a>
</p>
</form>
);
}
Protected Routes
import { Navigate, useLocation } from 'react-router-dom';
// Protected route component
function RequireAuth({ children }) {
const { user } = useAuth(); // Custom auth hook
const location = useLocation();
if (!user) {
// Redirect to login, but remember where they wanted to go
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Usage in routes
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/dashboard" element={
<RequireAuth>
<DashboardLayout />
</RequireAuth>
}>
<Route index element={<Overview />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Redirect back after login
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/dashboard';
async function handleLogin(event) {
event.preventDefault();
await login(event.target);
navigate(from, { replace: true });
}
return (
<form onSubmit={handleLogin}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
<p>Redirected from: {from}</p>
</form>
);
}
Lazy Loading Routes
import { lazy, Suspense } from 'react';
// Lazy load page components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Users = lazy(() => import('./pages/Users'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="loading">Loading page...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/users" element={<Users />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Common Mistakes
- Not wrapping routes in Suspense with lazy loading. React Router does not automatically handle loading states for lazy-loaded routes. Always provide a Suspense fallback.
- Forgetting the index route for nested layouts. The index route renders when the parent path matches exactly. Without it, the Outlet renders nothing.
- Using tags instead of . Regular anchor tags cause full page reloads in SPAs. Always use Link or NavLink for internal navigation.
- Not handling the wildcard route. Always include a Route with path="*" to catch unmatched routes and show a 404 page.
- Overusing useNavigate for simple links. Use the Link component for navigation. Reserve useNavigate for programmatic navigation after async operations.
Practice Questions
- What is the difference between BrowserRouter and HashRouter in React Router?
- How do nested routes work with the Outlet component?
- How do you access URL parameters and query parameters?
- How do you implement authentication guards for routes?
- How does lazy loading work with React Router?
Challenge: Build a React Router v6 application with: a public section (home, about), an authenticated dashboard with nested layout (overview, users/:id, settings), lazy-loaded pages, login redirect with return URL, and a 404 catch-all.
FAQ
{{< faq "How do I pass props to route components?" "Pass props using the element prop: <Route path="/user" element={
Mini Project
Build a multi-page React SPA with React Router v6: a landing page, a blog section with article list and article detail routes, an admin dashboard with nested routes (statistics, posts, settings), authentication guard on admin routes, lazy loading for dashboard pages, and a 404 page.
What's Next
You mastered React Router. Now learn Vue Router for Vue.js SPAs, or explore state management patterns for managing application data across routes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro