Single-Page Applications (SPAs)
A Single-Page Application (SPA) is a web application that loads a single HTML page and dynamically updates content as the user interacts with the app. React Router enables you to create SPAs with multiple "pages" that feel like traditional websites.
🔄 How it works:
- User clicks a link
- Browser makes new HTTP request
- Server sends complete new HTML page
- Page refreshes entirely
- JavaScript/CSS reload
❌ Disadvantages:
- Slower navigation
- Full page reloads
- State is lost between pages
- More server requests
⚡ How it works:
- User clicks a link
- JavaScript updates the URL
- React Router renders new component
- Only content area changes
- No page refresh needed
✅ Advantages:
- Faster navigation
- Smooth transitions
- State preservation
- Better user experience
React Router Setup
React Router is the most popular routing library for React applications. It provides declarative routing that keeps your UI in sync with the URL.
📦 Installation and Basic Setup
# Install React Router
npm install react-router-dom
# Or with yarn
yarn add react-router-dom
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// Page Components
function Home() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to our React application!</p>
</div>
);
}
function About() {
return (
<div>
<h1>About Page</h1>
<p>Learn more about our company.</p>
</div>
);
}
function Contact() {
return (
<div>
<h1>Contact Page</h1>
<p>Get in touch with us!</p>
</div>
);
}
// Navigation Component
function Navigation() {
return (
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
);
}
// Main App Component
function App() {
return (
<BrowserRouter>
<div>
<Navigation />
{/* Define Routes */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
Dynamic Routes and Parameters
Dynamic routes allow you to create flexible URLs that accept parameters. This is essential for pages like user profiles, product details, or blog posts.
🔗 URL Parameters
import { useParams, useNavigate } from 'react-router-dom';
// User Profile Component
function UserProfile() {
const { userId } = useParams(); // Get URL parameter
const navigate = useNavigate(); // Programmatic navigation
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch user data based on userId
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading user...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<button onClick={() => navigate('/users')}>
← Back to Users
</button>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Joined: {user.joinDate}</p>
<button onClick={() => navigate(`/users/${userId}/edit`)}>
Edit Profile
</button>
</div>
);
}
// Product Details with Multiple Parameters
function ProductDetails() {
const { category, productId } = useParams();
return (
<div>
<h1>Product Details</h1>
<p>Category: {category}</p>
<p>Product ID: {productId}</p>
</div>
);
}
// App with Dynamic Routes
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<UserList />} />
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/users/:userId/edit" element={<EditProfile />} />
<Route path="/products/:category/:productId" element={<ProductDetails />} />
<Route path="*" element={<NotFound />} /> {/* Catch-all route */}
</Routes>
</BrowserRouter>
);
}
Nested Routes
Nested routes allow you to build complex layouts where parts of the UI remain constant while other parts change based on the URL.
🏗️ Building Nested Layouts
import { Outlet, NavLink } from 'react-router-dom';
// Dashboard Layout Component
function Dashboard() {
return (
<div className="dashboard">
<aside className="sidebar">
<h2>Dashboard</h2>
<nav>
<NavLink
to="/dashboard"
end
className={({ isActive }) => isActive ? 'active' : ''}
>
Overview
</NavLink>
<NavLink
to="/dashboard/profile"
className={({ isActive }) => isActive ? 'active' : ''}
>
Profile
</NavLink>
<NavLink
to="/dashboard/settings"
className={({ isActive }) => isActive ? 'active' : ''}
>
Settings
</NavLink>
<NavLink
to="/dashboard/analytics"
className={({ isActive }) => isActive ? 'active' : ''}
>
Analytics
</NavLink>
</nav>
</aside>
<main className="main-content">
{/* Nested routes will render here */}
<Outlet />
</main>
</div>
);
}
// Dashboard Child Components
function DashboardOverview() {
return (
<div>
<h1>Dashboard Overview</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Users</h3>
<p>1,234</p>
</div>
<div className="stat-card">
<h3>Revenue</h3>
<p>$12,345</p>
</div>
</div>
</div>
);
}
function DashboardProfile() {
return (
<div>
<h1>Profile Settings</h1>
<form>
<input type="text" placeholder="Name" />
<input type="email" placeholder="Email" />
<button type="submit">Save Changes</button>
</form>
</div>
);
}
function DashboardSettings() {
return (
<div>
<h1>Application Settings</h1>
<div>
<label>
<input type="checkbox" /> Enable Notifications
</label>
<label>
<input type="checkbox" /> Dark Mode
</label>
</div>
</div>
);
}
// App with Nested Routes
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Nested Dashboard Routes */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardOverview />} /> {/* Default child */}
<Route path="profile" element={<DashboardProfile />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="analytics" element={<DashboardAnalytics />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Navigation and Router Hooks
🧭 useNavigate
Programmatically navigate to different routes
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleLogin = async (credentials) => {
try {
await login(credentials);
// Navigate to dashboard after successful login
navigate('/dashboard');
// Or navigate with replacement (can't go back)
navigate('/dashboard', { replace: true });
// Navigate back
navigate(-1);
// Navigate with state
navigate('/dashboard', {
state: { from: '/login' }
});
} catch (error) {
console.error('Login failed');
}
};
return (
<form onSubmit={handleLogin}>
{/* form fields */}
</form>
);
}
📍 useLocation
Access current location and state
import { useLocation } from 'react-router-dom';
function CurrentPage() {
const location = useLocation();
useEffect(() => {
// Track page views
analytics.track('Page View', {
path: location.pathname,
search: location.search
});
}, [location]);
// Access state passed from navigate
const fromPage = location.state?.from;
return (
<div>
<p>Current path: {location.pathname}</p>
<p>Query string: {location.search}</p>
{fromPage && <p>Came from: {fromPage}</p>}
</div>
);
}
🔍 useSearchParams
Read and update URL query parameters
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, newSort) => {
setSearchParams({
category: newCategory,
sort: newSort,
page: '1' // Reset to page 1
});
};
return (
<div>
<select
value={category}
onChange={(e) => updateFilters(e.target.value, sort)}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
</select>
<select
value={sort}
onChange={(e) => updateFilters(category, e.target.value)}
>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
{/* URL will be: /products?category=electronics&sort=price&page=1 */}
</div>
);
}
🛡️ Protected Routes
Restrict access based on authentication
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const { user } = useAuth(); // Custom auth hook
const location = useLocation();
if (!user) {
// Redirect to login with return path
return (
<Navigate
to="/login"
state={{ from: location.pathname }}
replace
/>
);
}
return children;
}
// Usage in App
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
);
}
Routing Best Practices
✅ Route Organization
- Use clear, descriptive URLs
- Group related routes together
- Implement consistent naming patterns
- Use nested routes for layouts
- Always include a 404 catch-all route
🚀 Performance
- Use React.lazy for code splitting
- Implement loading states
- Preload critical routes
- Use suspense boundaries
- Cache route data when appropriate
🛡️ Security
- Validate route parameters
- Implement proper authentication
- Use protected route components
- Sanitize URL parameters
- Handle unauthorized access gracefully
♿ Accessibility
- Update page titles on route changes
- Manage focus after navigation
- Provide skip navigation links
- Use semantic navigation elements
- Support keyboard navigation
What's Next?
You now understand how to build single-page applications with React Router. Next, you'll explore the React ecosystem and essential development tools.
🚀 Continue Learning
- Development Tools - Essential tools for React development
- Next.js - Full-stack React framework with built-in routing
- Popular Libraries - UI libraries and ecosystem tools
- Transition Guide - Complete migration strategies