React Router
React Router is the standard routing library for React. It enables navigation between views, URL parameter handling, and keeps your UI in sync with the URL—all without page reloads.Why Client-Side Routing?
Copy
Traditional Server Routing:
┌──────────────────────────────────────────────────────────┐
│ Click link → Request to server → Full page reload │
│ │
│ Slow, loses client state, full re-render │
└──────────────────────────────────────────────────────────┘
Client-Side Routing (SPA):
┌──────────────────────────────────────────────────────────┐
│ Click link → JavaScript updates URL → Re-render view │
│ │
│ Fast, keeps state, smooth transitions │
└──────────────────────────────────────────────────────────┘
Installation
Copy
npm install react-router-dom
Basic Setup
1. Wrap Your App with BrowserRouter
Copy
// main.jsx
import { BrowserRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
2. Define Routes
Copy
// App.jsx
import { Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
function App() {
return (
<div>
{/* Navigation */}
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
{/* Route Definitions */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
Link vs NavLink
Link - Basic Navigation
Copy
import { Link } from 'react-router-dom';
<Link to="/about">About Us</Link>
<Link to="/products?category=electronics">Electronics</Link>
NavLink - Active State Styling
Copy
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Home
</NavLink>
<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'blue' : 'gray'
})}
>
About
</NavLink>
</nav>
);
}
Dynamic Routes (URL Parameters)
Defining Dynamic Routes
Copy
<Routes>
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/products/:category/:productId" element={<ProductDetail />} />
</Routes>
Accessing Parameters with useParams
Copy
import { useParams } from 'react-router-dom';
function UserProfile() {
const { userId } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <Loading />;
return (
<div>
<h1>{user.name}</h1>
<p>User ID: {userId}</p>
</div>
);
}
Query Parameters (Search Params)
Copy
import { useSearchParams } from 'react-router-dom';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const page = parseInt(searchParams.get('page')) || 1;
const updateFilters = (newCategory) => {
setSearchParams({
category: newCategory,
sort,
page: 1 // Reset to page 1 on filter change
});
};
return (
<div>
<p>Current URL: /products?category={category}&sort={sort}&page={page}</p>
<div className="filters">
<button onClick={() => updateFilters('electronics')}>Electronics</button>
<button onClick={() => updateFilters('clothing')}>Clothing</button>
</div>
<select
value={sort}
onChange={(e) => setSearchParams({ category, sort: e.target.value, page })}
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
);
}
Programmatic Navigation
useNavigate Hook
Copy
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(credentials);
// Navigate to dashboard
navigate('/dashboard');
// Or with replace (no back button)
navigate('/dashboard', { replace: true });
// Or go back
navigate(-1);
// Pass state
navigate('/dashboard', { state: { from: 'login' } });
} catch (err) {
setError('Login failed');
}
};
return (/* form */);
}
Accessing Navigation State
Copy
import { useLocation } from 'react-router-dom';
function Dashboard() {
const location = useLocation();
const from = location.state?.from;
return (
<div>
{from === 'login' && <p>Welcome! You just logged in.</p>}
</div>
);
}
Nested Routes
Create layouts with shared navigation:Copy
// App.jsx
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Copy
// Layout.jsx
import { Outlet } from 'react-router-dom';
function Layout() {
return (
<div>
<Header />
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
{/* Renders child routes */}
<main>
<Outlet />
</main>
<Footer />
</div>
);
}
Copy
// DashboardLayout.jsx
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<NavLink to="/dashboard" end>Overview</NavLink>
<NavLink to="/dashboard/settings">Settings</NavLink>
<NavLink to="/dashboard/profile">Profile</NavLink>
</aside>
<section>
<Outlet />
</section>
</div>
);
}
The
end prop on NavLink ensures it only matches exactly, not partial paths.Protected Routes
Basic Protected Route
Copy
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext';
function ProtectedRoute() {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
// Save the attempted URL for redirecting after login
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
Using Protected Routes
Copy
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
{/* Protected Routes */}
<Route element={<ProtectedRoute />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
);
}
Role-Based Protection
Copy
function RoleProtectedRoute({ allowedRoles }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (!allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return <Outlet />;
}
// Usage
<Route element={<RoleProtectedRoute allowedRoles={['admin']} />}>
<Route path="admin" element={<AdminDashboard />} />
</Route>
Lazy Loading Routes
Load routes only when needed:Copy
import { lazy, Suspense } from 'react';
// Lazy load pages
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route
path="dashboard"
element={
<Suspense fallback={<PageLoader />}>
<Dashboard />
</Suspense>
}
/>
<Route
path="settings"
element={
<Suspense fallback={<PageLoader />}>
<Settings />
</Suspense>
}
/>
</Route>
</Routes>
);
}
Shared Suspense Boundary
Copy
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</Suspense>
);
}
useLocation Hook
Access current location information:Copy
import { useLocation } from 'react-router-dom';
function Breadcrumbs() {
const location = useLocation();
// location = {
// pathname: '/dashboard/settings',
// search: '?tab=security',
// hash: '#password',
// state: { from: 'profile' },
// key: 'abc123'
// }
const pathSegments = location.pathname.split('/').filter(Boolean);
return (
<nav className="breadcrumbs">
<Link to="/">Home</Link>
{pathSegments.map((segment, index) => {
const path = '/' + pathSegments.slice(0, index + 1).join('/');
return (
<span key={path}>
{' / '}
<Link to={path}>{segment}</Link>
</span>
);
})}
</nav>
);
}
Scroll Restoration
Reset scroll position on navigation:Copy
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
// In App.jsx
function App() {
return (
<BrowserRouter>
<ScrollToTop />
<Routes>...</Routes>
</BrowserRouter>
);
}
Error Boundaries with Routes
Copy
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return <NotFoundPage />;
}
if (error.status === 401) {
return <UnauthorizedPage />;
}
}
return (
<div className="error-page">
<h1>Oops! Something went wrong</h1>
<p>{error.message || 'Unknown error occurred'}</p>
</div>
);
}
// In route configuration
<Route
path="/"
element={<Layout />}
errorElement={<ErrorPage />}
>
{/* routes */}
</Route>
🎯 Practice Exercises
Exercise 1: Blog with Dynamic Routes
Exercise 1: Blog with Dynamic Routes
Copy
// App.jsx
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="blog">
<Route index element={<BlogList />} />
<Route path=":slug" element={<BlogPost />} />
<Route path="category/:category" element={<BlogCategory />} />
</Route>
</Route>
</Routes>
);
}
// BlogList.jsx
function BlogList() {
const posts = [
{ slug: 'react-basics', title: 'React Basics', category: 'react' },
{ slug: 'hooks-intro', title: 'Intro to Hooks', category: 'react' },
{ slug: 'css-grid', title: 'CSS Grid Layout', category: 'css' }
];
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.slug}>
<Link to={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</Link>
<Link to={`/blog/category/${post.category}`}>
{post.category}
</Link>
</article>
))}
</div>
);
}
// BlogPost.jsx
function BlogPost() {
const { slug } = useParams();
const navigate = useNavigate();
// Fetch post by slug...
return (
<article>
<button onClick={() => navigate(-1)}>← Back</button>
<h1>Post: {slug}</h1>
</article>
);
}
Exercise 2: Tab-Based Navigation with Query Params
Exercise 2: Tab-Based Navigation with Query Params
Copy
function SettingsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const currentTab = searchParams.get('tab') || 'general';
const tabs = [
{ id: 'general', label: 'General', component: GeneralSettings },
{ id: 'security', label: 'Security', component: SecuritySettings },
{ id: 'notifications', label: 'Notifications', component: NotificationSettings }
];
const ActiveComponent = tabs.find(t => t.id === currentTab)?.component || GeneralSettings;
return (
<div className="settings-page">
<h1>Settings</h1>
<div className="tabs">
{tabs.map(tab => (
<button
key={tab.id}
className={currentTab === tab.id ? 'active' : ''}
onClick={() => setSearchParams({ tab: tab.id })}
>
{tab.label}
</button>
))}
</div>
<div className="tab-content">
<ActiveComponent />
</div>
</div>
);
}
Summary
| Concept | Description |
|---|---|
| BrowserRouter | Wraps app for routing |
| Routes & Route | Define route paths and elements |
| Link / NavLink | Navigation without page reload |
| useParams | Access URL parameters |
| useSearchParams | Access and set query parameters |
| useNavigate | Programmatic navigation |
| useLocation | Current location info |
| Outlet | Render child routes |
| Protected Routes | Auth-guarded routes |
| Lazy Loading | Load routes on demand |
Next Steps
In the next chapter, you’ll learn about Optimization & Deployment — making your React apps fast and production-ready!