React Router Best Practices
Introduction
React Router is the standard library for routing in React applications. While implementing routing may seem straightforward, following best practices ensures your application remains maintainable, performs well, and provides a great user experience. This guide will walk you through the most important React Router best practices that will help you avoid common pitfalls and create robust routing systems in your React applications.
Prerequisites
Before diving into best practices, make sure you have:
- Basic knowledge of React
- React Router installed in your project:
npm install react-router-dom
1. Organize Routes in a Centralized Configuration
The Problem
Scattering route definitions throughout your application can make it difficult to maintain and understand your routing structure.
Best Practice: Centralize Route Configuration
Create a dedicated file (e.g., routes.js
) to define all your routes:
// src/routes.js
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import NotFound from './pages/NotFound';
const routes = [
{
path: '/',
element: <Home />,
exact: true,
},
{
path: '/about',
element: <About />,
},
{
path: '/products',
element: <Products />,
},
{
path: '/products/:id',
element: <ProductDetail />,
},
{
path: '*',
element: <NotFound />,
},
];
export default routes;
Then, use this configuration in your main app:
// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import routes from './routes';
function App() {
return (
<BrowserRouter>
<Routes>
{routes.map((route, index) => (
<Route
key={index}
path={route.path}
element={route.element}
exact={route.exact}
/>
))}
</Routes>
</BrowserRouter>
);
}
export default App;
Benefits
- Single source of truth for routes
- Easier to understand the overall application structure
- Simplified route maintenance
2. Use Layout Components for Consistent UI
Best Practice: Create Nested Layouts
Use layout components to maintain consistent UI elements across related pages:
// src/layouts/MainLayout.js
import { Outlet } from 'react-router-dom';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
function MainLayout() {
return (
<div className="main-layout">
<Navbar />
<main>
<Outlet /> {/* Child routes will render here */}
</main>
<Footer />
</div>
);
}
export default MainLayout;
Then use this layout with your routes:
// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
3. Implement Lazy Loading for Routes
The Problem
Loading all route components upfront can increase the initial bundle size, slowing down the first load of your application.
Best Practice: Use React.lazy and Suspense
// src/App.js
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Loading from './components/Loading';
// Lazy load components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const NotFound = lazy(() => import('./pages/NotFound'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />} />
<Route path="products/:id" element={<ProductDetail />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;
Benefits
- Reduced initial bundle size
- Faster initial load time
- Better performance for users
4. Create Reusable Navigation Components
Best Practice: Build Flexible Navigation Components
// src/components/Navigation.js
import { NavLink } from 'react-router-dom';
function Navigation({ links }) {
return (
<nav className="main-nav">
<ul>
{links.map((link) => (
<li key={link.to}>
<NavLink
to={link.to}
className={({ isActive }) => isActive ? 'active-link' : ''}
>
{link.label}
</NavLink>
</li>
))}
</ul>
</nav>
);
}
export default Navigation;
Usage:
// src/components/Navbar.js
import Navigation from './Navigation';
function Navbar() {
const navLinks = [
{ to: '/', label: 'Home' },
{ to: '/about', label: 'About' },
{ to: '/products', label: 'Products' },
{ to: '/contact', label: 'Contact' },
];
return (
<header>
<div className="logo">My App</div>
<Navigation links={navLinks} />
</header>
);
}
export default Navbar;
5. Handle Route Parameters Properly
Best Practice: Use Hooks for Route Parameters
// src/pages/ProductDetail.js
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';
function ProductDetail() {
const { id } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
// Replace with your actual API call
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error('Product not found');
}
const data = await response.json();
setProduct(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [id]);
if (loading) return <div>Loading product...</div>;
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={() => navigate('/products')}>
Back to Products
</button>
</div>
);
}
return (
<div className="product-detail">
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="price">${product.price}</p>
<button onClick={() => navigate('/products')}>
Back to Products
</button>
</div>
);
}
export default ProductDetail;
6. Implement Protected Routes
Best Practice: Create a Reusable Protected Route Component
// src/components/ProtectedRoute.js
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext'; // Your auth context
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// Redirect to login page but save the attempted URL
return <Navigate to="/login" state={{ from: location.pathname }} replace />;
}
return children;
}
export default ProtectedRoute;
Usage in your routes:
// src/App.js
// ... imports
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route
path="dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
7. Handle 404 Pages Gracefully
Best Practice: Create a Good 404 Page Experience
// src/pages/NotFound.js
import { Link, useLocation } from 'react-router-dom';
function NotFound() {
const location = useLocation();
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>Sorry, no page exists at <code>{location.pathname}</code></p>
<div>
<Link to="/" className="home-link">
Return to Homepage
</Link>
</div>
</div>
);
}
export default NotFound;
Make sure to include a catch-all route in your route configuration:
<Route path="*" element={<NotFound />} />
8. Use React Router Hooks Effectively
Best Practice: Leverage React Router's Hooks
React Router provides several hooks that make navigation and accessing route parameters easier:
// src/components/ProductActions.js
import { useNavigate, useParams, useLocation } from 'react-router-dom';
function ProductActions({ product }) {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();
// Keep track of where the user came from
const handleEdit = () => {
navigate(`/products/${id}/edit`, { state: { returnTo: location.pathname } });
};
const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete this product?')) {
// Delete logic here
navigate('/products');
}
};
const handleBack = () => {
navigate(-1); // Go back to the previous page
};
return (
<div className="product-actions">
<button onClick={handleEdit}>Edit</button>
<button onClick={handleDelete}>Delete</button>
<button onClick={handleBack}>Back</button>
</div>
);
}
export default ProductActions;
9. Handle Query Parameters
Best Practice: Use useSearchParams Hook
// src/pages/Products.js
import { useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';
function Products() {
const [searchParams, setSearchParams] = useSearchParams();
const [products, setProducts] = useState([]);
// Get query parameters
const category = searchParams.get('category') || 'all';
const page = parseInt(searchParams.get('page') || '1');
useEffect(() => {
// Fetch products based on query parameters
fetchProducts(category, page);
}, [category, page]);
const fetchProducts = async (category, page) => {
// Your fetch logic here
// ...
};
const handleCategoryChange = (newCategory) => {
setSearchParams({ category: newCategory, page: '1' });
};
const handlePageChange = (newPage) => {
setSearchParams({ category, page: newPage.toString() });
};
return (
<div>
<h1>Products</h1>
{/* Category filter */}
<div className="categories">
<button
onClick={() => handleCategoryChange('all')}
className={category === 'all' ? 'active' : ''}
>
All
</button>
<button
onClick={() => handleCategoryChange('electronics')}
className={category === 'electronics' ? 'active' : ''}
>
Electronics
</button>
<button
onClick={() => handleCategoryChange('clothing')}
className={category === 'clothing' ? 'active' : ''}
>
Clothing
</button>
</div>
{/* Products list */}
<div className="products-list">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
))}
</div>
{/* Pagination */}
<div className="pagination">
<button
onClick={() => handlePageChange(page - 1)}
disabled={page <= 1}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => handlePageChange(page + 1)}>
Next
</button>
</div>
</div>
);
}
export default Products;
10. Implement Route Transitions
Best Practice: Add Smooth Transitions Between Routes
You can use libraries like framer-motion
to create smooth transitions:
// First install: npm install framer-motion
// src/components/PageTransition.js
import { motion } from 'framer-motion';
const pageVariants = {
initial: {
opacity: 0,
x: '-5vw',
},
in: {
opacity: 1,
x: 0,
},
out: {
opacity: 0,
x: '5vw',
}
};
const pageTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.4
};
function PageTransition({ children }) {
return (
<motion.div
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
{children}
</motion.div>
);
}
export default PageTransition;
Then wrap your page components:
// src/pages/Home.js
import PageTransition from '../components/PageTransition';
function Home() {
return (
<PageTransition>
<div className="home-page">
<h1>Welcome to Our Website</h1>
{/* Content here */}
</div>
</PageTransition>
);
}
export default Home;
To make it work with React Router v6, you'll need to use AnimatePresence
from framer-motion:
// src/App.js
import { Routes, Route, useLocation } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
// ... other imports
function App() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
{/* Your routes here */}
</Routes>
</AnimatePresence>
);
}
11. Create a Navigation Progress Bar
Best Practice: Add a Loading Indicator for Navigation
Using a library like nprogress
:
// First install: npm install nprogress
// src/components/NavigationProgress.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
// Custom styles for the progress bar
import './NavigationProgress.css';
function NavigationProgress() {
const location = useLocation();
useEffect(() => {
NProgress.start();
// Simulate a delay to show the progress bar
const timer = setTimeout(() => {
NProgress.done();
}, 500);
return () => {
clearTimeout(timer);
NProgress.done();
};
}, [location.pathname]);
return null; // This component doesn't render anything
}
export default NavigationProgress;
Then include it in your app:
// src/App.js
import NavigationProgress from './components/NavigationProgress';
function App() {
return (
<BrowserRouter>
<NavigationProgress />
{/* Rest of your app */}
</BrowserRouter>
);
}
Summary
In this guide, we covered the essential best practices for React Router:
- Organize Routes Centrally: Keep all route definitions in one place
- Use Layout Components: Maintain consistent UI elements across pages
- Implement Lazy Loading: Improve initial load times with code splitting
- Create Reusable Navigation Components: Build flexible navigation systems
- Handle Route Parameters Properly: Use React Router hooks for clean code
- Implement Protected Routes: Secure parts of your application
- Handle 404 Pages Gracefully: Create user-friendly error pages
- Use React Router Hooks Effectively: Leverage built-in hooks for clean code
- Handle Query Parameters: Manage filters and pagination state in the URL
- Implement Route Transitions: Add smooth animations between routes
- Create a Navigation Progress Bar: Improve perceived performance
Following these best practices will help you build maintainable React applications with clear navigation patterns and improved user experience.
Additional Resources
Exercises
- Refactor an existing React app to use a centralized route configuration
- Implement lazy loading for routes in your application
- Create a protected route system with login/logout functionality
- Add route transitions with Framer Motion
- Implement a filter and pagination system using query parameters
By implementing these practices in your React applications, you'll create more maintainable, performant, and user-friendly experiences.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)