Introduction to Routing
In traditional websites, navigation between different pages requires sending a request to the server, which responds with the new HTML page. This process causes a full page reload each time a user navigates to a different page.
Single-Page Applications (SPAs) like those built with React work differently. They load a single HTML page and then dynamically update the content as users interact with the app, without requiring full page reloads. This approach provides a smoother, more app-like user experience.
Client-side routing allows a React application to:
- Update the URL as users navigate through the app
- Match different components to different URLs
- Handle browser history (back/forward buttons)
- Parse and utilize URL parameters
- Navigate programmatically
- All without requiring a full page reload
Real-World Analogy: TV Channels vs. Streaming Service
Think of the traditional web like a TV with different channels:
- Each channel (URL) is a completely separate content source
- Changing channels (navigating) causes a moment of "blank screen" (page reload)
- Each channel request goes through the broadcasting station (server)
Client-side routing in SPAs is more like a modern streaming service:
- The app itself (streaming device) is already loaded
- Navigation just tells the app which content to display
- Transitions between content are smooth and controlled
- Your viewing history is maintained within the app
- You can quickly switch between recently viewed content
Introducing React Router
React Router is the standard routing library for React applications. It keeps your UI in sync with the URL by declaratively rendering components based on the current URL path.
Key features of React Router include:
- Declarative routing with JSX
- Dynamic route matching
- Nested routes and layouts
- URL parameters and query string handling
- History management
- Route-specific code splitting
Let's install React Router in your project:
# npm
npm install react-router-dom
# yarn
yarn add react-router-dom
The current stable version of React Router is v6, which introduced several changes and improvements over v5. This lecture will focus on React Router v6.
React Router Core Concepts
Understanding React Router requires familiarity with a few core concepts:
- BrowserRouter: The root component that provides routing context
- Routes: A container for Route components that defines which component renders for each path
- Route: Maps a URL path to a component
- Link: Creates navigation links that don't trigger page reloads
- Outlet: Defines where child routes should be rendered
- Navigate: A component for programmatic navigation
- Hooks: Utilities like useParams, useNavigate, useLocation for accessing routing information
Setting Up Basic Routing
Let's set up a simple routing system for a React application:
// index.js or App.js
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
import Navbar from './components/Navbar';
function App() {
return (
<BrowserRouter>
<div>
<Navbar />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
}
export default App;
In this example:
BrowserRouteris the wrapper that enables routing in your appRoutesis a container for all yourRoutecomponents- Each
Routemaps a path to a component using theelementprop - The
path="*"route acts as a catch-all for any URLs that don't match defined routes
Creating Navigation Links
The Link component creates navigation links that use client-side routing instead of traditional page navigation:
// components/Navbar.js
import React from 'react';
import { Link } from 'react-router-dom';
function Navbar() {
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>
);
}
export default Navbar;
The Link component:
- Renders as an
<a>tag in the HTML - Intercepts clicks to prevent default navigation
- Updates the URL without triggering a page reload
- Allows React Router to render the appropriate component
NavLink: Enhanced Links with Active Styling
React Router also provides a NavLink component that extends Link with the ability to style the active link:
import { NavLink } from 'react-router-dom';
function Navbar() {
// Function to determine active styles
const navLinkStyles = ({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? '#0077cc' : '#333333',
textDecoration: isActive ? 'underline' : 'none'
});
return (
<nav>
<ul>
<li>
<NavLink to="/" style={navLinkStyles}>
Home
</NavLink>
</li>
<li>
<NavLink to="/about" style={navLinkStyles}>
About
</NavLink>
</li>
<li>
<NavLink to="/contact" style={navLinkStyles}>
Contact
</NavLink>
</li>
</ul>
</nav>
);
}
Alternatively, you can use the className prop:
<NavLink
to="/"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
Home
</NavLink>
URL Parameters and Dynamic Routes
Most applications need dynamic routes that can match different URLs with the same pattern. For example, product detail pages often have URLs like /products/1, /products/2, etc.
React Router handles this with URL parameters, denoted by a colon in the path:
<Route path="/products/:id" element={<ProductDetail />} />
Inside the ProductDetail component, we can access this parameter using the useParams hook:
// pages/ProductDetail.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
function ProductDetail() {
// Extract the id parameter from the URL
const { id } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch product data based on the ID
async function fetchProduct() {
try {
setLoading(true);
const response = await fetch(`/api/products/${id}`);
const data = await response.json();
setProduct(data);
} catch (error) {
console.error('Error fetching product:', error);
} finally {
setLoading(false);
}
}
fetchProduct();
}, [id]); // Re-fetch when the ID changes
if (loading) {
return <div>Loading...</div>;
}
if (!product) {
return <div>Product not found</div>;
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<div>Price: ${product.price}</div>
</div>
);
}
export default ProductDetail;
You can have multiple parameters in a single route:
<Route path="/categories/:categoryId/products/:productId" element={<CategoryProduct />} />
// In the component
function CategoryProduct() {
const { categoryId, productId } = useParams();
// ...
}
Practical Example: E-commerce Product Pages
In an e-commerce application, you might have routes like:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:productId" element={<ProductDetailPage />} />
<Route path="/categories/:categoryId" element={<CategoryPage />} />
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/order-confirmation/:orderId" element={<OrderConfirmationPage />} />
</Routes>
Creating links to dynamic routes:
function ProductCard({ product }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<Link to={`/products/${product.id}`}>View Details</Link>
</div>
);
}
Query Parameters
Unlike URL parameters, query parameters (the part after ? in a URL) don't affect route matching. They're optional and often used for filtering, sorting, or pagination.
React Router provides the useSearchParams hook to work with query parameters:
// pages/ProductList.js
import React from 'react';
import { useSearchParams } from 'react-router-dom';
function ProductList() {
// Access query parameters (e.g., ?category=electronics&sort=price)
const [searchParams, setSearchParams] = useSearchParams();
// Get individual parameters
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
// Update query parameters
const handleCategoryChange = (e) => {
const newCategory = e.target.value;
setSearchParams({ category: newCategory, sort });
};
const handleSortChange = (e) => {
const newSort = e.target.value;
setSearchParams({ category, sort: newSort });
};
return (
<div>
<h1>Products</h1>
<div>
<label>
Category:
<select value={category} onChange={handleCategoryChange}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
</label>
<label>
Sort by:
<select value={sort} onChange={handleSortChange}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="newest">Newest</option>
</select>
</label>
</div>
<div>
<p>Showing {category} products sorted by {sort}</p>
{/* Product list would go here */}
</div>
</div>
);
}
export default ProductList;
When the user changes a filter, setSearchParams updates the URL, which allows users to:
- Share filtered/sorted views via URL
- Bookmark specific views
- Use browser history to navigate between different filter states
Creating links with query parameters
You can also create links with predefined query parameters:
<Link to="/products?category=electronics&sort=price">
View Electronics (Price Low-High)
</Link>
// Or build them dynamically
function CategoryLink({ category, sort = 'name', children }) {
return (
<Link to={`/products?category=${category}&sort=${sort}`}>
{children}
</Link>
);
}
// Usage
<CategoryLink category="electronics">
View All Electronics
</CategoryLink>
For more complex query parameters, you can use URLSearchParams:
function buildProductsUrl(filters) {
const params = new URLSearchParams();
if (filters.category) {
params.append('category', filters.category);
}
if (filters.sort) {
params.append('sort', filters.sort);
}
if (filters.minPrice) {
params.append('minPrice', filters.minPrice);
}
if (filters.maxPrice) {
params.append('maxPrice', filters.maxPrice);
}
// Convert to string: "category=electronics&sort=price&minPrice=100"
const queryString = params.toString();
// Return the full path
return `/products${queryString ? `?${queryString}` : ''}`;
}
// Usage
const filtersUrl = buildProductsUrl({
category: 'electronics',
sort: 'price',
minPrice: 100
});
<Link to={filtersUrl}>Filtered Electronics</Link>
Programmatic Navigation
While Link components are perfect for declarative navigation, sometimes you need to navigate programmatically, such as after form submissions or based on certain conditions.
React Router provides the useNavigate hook for this purpose:
// pages/LoginForm.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Call login API
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// Login successful
// Store user data, token, etc.
localStorage.setItem('token', data.token);
// Redirect to dashboard
navigate('/dashboard');
} else {
// Handle login error
alert(data.message || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
alert('An error occurred. Please try again.');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Log In</button>
<p>
<button
type="button"
onClick={() => navigate('/signup')}
>
Create Account
</button>
</p>
</form>
);
}
export default LoginForm;
Advanced Navigation Options
The navigate function provides several options:
// Basic navigation
navigate('/dashboard');
// Navigate with replacement (no back button entry)
navigate('/dashboard', { replace: true });
// Navigate relative to the current route
navigate('../parent'); // Go up one level
navigate('child'); // Go to a child route
navigate('.'); // Stay at the same route but refresh
// Navigate with state (data that persists with navigation)
navigate('/product-detail', {
state: { productData: product }
});
// Navigate back
navigate(-1);
// Navigate forward
navigate(1);
// Go back two pages
navigate(-2);
Real-World Navigation Scenarios
Here are some common scenarios where programmatic navigation is useful:
- Form Submissions: Redirect after successful form submission
- Authentication: Redirect to login or dashboard based on auth state
- Checkout Flow: Guide users through a multi-step process
- Access Control: Redirect unauthorized users away from protected routes
- Timeouts: Redirect after a period of inactivity
- Confirmation Pages: Navigate to confirmation after completing an action
Example: Redirecting to a confirmation page after an order is placed:
function CheckoutPage() {
const navigate = useNavigate();
const [cart, setCart] = useState(/* ... */);
const handlePlaceOrder = async () => {
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ items: cart.items })
});
const order = await response.json();
if (response.ok) {
// Clear the cart
setCart({ items: [] });
// Navigate to order confirmation page with order data
navigate(`/order-confirmation/${order.id}`, {
state: { orderDetails: order }
});
} else {
alert(order.message || 'Failed to place order');
}
} catch (error) {
console.error('Order error:', error);
alert('An error occurred while placing your order');
}
};
// Rest of component...
}
Accessing Route Information
React Router provides several hooks to access information about the current route:
useLocation
The useLocation hook returns the current location object, which contains information about the current URL:
import { useLocation } from 'react-router-dom';
function Analytics() {
const location = useLocation();
// location object contains:
// - pathname: The path section of the URL
// - search: The query string (including '?')
// - hash: The hash (including '#')
// - state: Any state passed with navigate()
// - key: A unique ID for this location
useEffect(() => {
// Track page views
analytics.trackPageView({
path: location.pathname,
search: location.search,
timestamp: new Date().toISOString()
});
}, [location]);
return null; // This component doesn't render anything
}
useRouteMatch
The useMatch hook tests if the current URL matches a specific pattern:
import { useMatch } from 'react-router-dom';
function ProductBreadcrumb() {
// Check if we're on a product detail page
const productMatch = useMatch('/products/:productId');
if (!productMatch) {
return <span>Products</span>;
}
// We are on a product detail page
const productId = productMatch.params.productId;
return (
<div className="breadcrumb">
<Link to="/products">Products</Link> >
<span>Product {productId}</span>
</div>
);
}
Accessing State from Navigation
When you navigate with state, you can access it in the destination component:
// Component that navigates with state
function ProductList() {
const navigate = useNavigate();
const handleProductClick = (product) => {
navigate(`/products/${product.id}`, {
state: {
productData: product,
fromSearch: true
}
});
};
// ...
}
// Component that receives the state
function ProductDetail() {
const location = useLocation();
const { productId } = useParams();
// Access the state passed during navigation
const productData = location.state?.productData;
const fromSearch = location.state?.fromSearch;
useEffect(() => {
// If we didn't receive product data with navigation,
// we need to fetch it
if (!productData) {
fetchProductData(productId);
}
}, [productId, productData]);
// Render different back links based on where user came from
const backLink = fromSearch ? (
<Link to="/search">Back to Search Results</Link>
) : (
<Link to="/products">Back to All Products</Link>
);
// ...
}
Practical Example: Product Detail with Location State
Let's see a complete example of passing product data via navigation state to avoid unnecessary API calls:
// ProductList.js
function ProductList() {
const [products, setProducts] = useState([]);
const navigate = useNavigate();
useEffect(() => {
// Fetch products when component mounts
async function fetchProducts() {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
}
fetchProducts();
}, []);
const handleProductClick = (product) => {
// Navigate to product detail with the full product data
navigate(`/products/${product.id}`, {
state: { productData: product }
});
};
return (
<div>
<h1>Products</h1>
<div className="product-grid">
{products.map(product => (
<div
key={product.id}
className="product-card"
onClick={() => handleProductClick(product)}
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
</div>
);
}
// ProductDetail.js
function ProductDetail() {
const { productId } = useParams();
const location = useLocation();
const navigate = useNavigate();
// Check if product data was passed in location state
const [product, setProduct] = useState(location.state?.productData || null);
const [loading, setLoading] = useState(!product);
const [error, setError] = useState(null);
useEffect(() => {
// Only fetch if we don't have product data already
if (!product) {
async function fetchProduct() {
try {
setLoading(true);
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('Product not found');
}
const data = await response.json();
setProduct(data);
setError(null);
} catch (err) {
setError(err.message);
setProduct(null);
} finally {
setLoading(false);
}
}
fetchProduct();
}
}, [productId, product]);
if (loading) {
return <div>Loading product details...</div>;
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={() => navigate('/products')}>
Back to Products
</button>
</div>
);
}
if (!product) {
return <div>Product not found</div>;
}
return (
<div>
<h1>{product.name}</h1>
<div className="product-detail">
<div className="product-image">
<img src={product.image} alt={product.name} />
</div>
<div className="product-info">
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<button>Add to Cart</button>
</div>
</div>
<Link to="/products">Back to Products</Link>
</div>
);
}
This pattern provides a smoother user experience by:
- Eliminating loading states when navigating from a list to a detail view
- Reducing unnecessary API calls
- Gracefully falling back to API fetching when needed (e.g., when accessing the detail page directly)
Practice Activities
Activity 1: Basic Routing Setup
Create a simple React application with the following pages:
- Home page with a welcome message
- About page with information about a fictional company
- Contact page with a contact form (no need to make it functional)
- 404 page for routes that don't match
Implement a navigation bar with links to each page and style the active link differently.
Activity 2: URL Parameters and Query Strings
Extend your application with:
- A products page that displays a list of products (mock data is fine)
- Each product should link to a product detail page using a URL parameter
- Add filtering capabilities to the products page using query parameters (category, sort, etc.)
- Implement UI controls to change these filters
Activity 3: Programmatic Navigation
Add the following features to your application:
- A login form that redirects to a dashboard page on successful login (mock the authentication)
- A checkout flow that guides users through multiple steps (cart → shipping → payment → confirmation)
- Add navigation buttons (Next/Back) between these steps
- Implement a "Continue Shopping" button on the confirmation page that navigates back to products
Summary
- React Router enables client-side routing in React applications
- Basic routing setup involves BrowserRouter, Routes, and Route components
- Link and NavLink components provide navigation without page reloads
- URL parameters enable dynamic routes with useParams hook
- Query parameters can be accessed and modified with useSearchParams
- Programmatic navigation is possible with the useNavigate hook
- Other hooks like useLocation provide access to route information
- Location state allows passing data between routes during navigation