Skip to main content

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:
bash
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:

jsx
// 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:

jsx
// 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:

jsx
// 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:

jsx
// 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

jsx
// 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

jsx
// 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:

jsx
// 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

jsx
// 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

jsx
// 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:

jsx
// 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

jsx
// 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:

jsx
<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:

jsx
// 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

jsx
// 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:

jsx
// 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:

jsx
// 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:

jsx
// 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:

jsx
// 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:

jsx
// 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:

  1. Organize Routes Centrally: Keep all route definitions in one place
  2. Use Layout Components: Maintain consistent UI elements across pages
  3. Implement Lazy Loading: Improve initial load times with code splitting
  4. Create Reusable Navigation Components: Build flexible navigation systems
  5. Handle Route Parameters Properly: Use React Router hooks for clean code
  6. Implement Protected Routes: Secure parts of your application
  7. Handle 404 Pages Gracefully: Create user-friendly error pages
  8. Use React Router Hooks Effectively: Leverage built-in hooks for clean code
  9. Handle Query Parameters: Manage filters and pagination state in the URL
  10. Implement Route Transitions: Add smooth animations between routes
  11. 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

  1. Refactor an existing React app to use a centralized route configuration
  2. Implement lazy loading for routes in your application
  3. Create a protected route system with login/logout functionality
  4. Add route transitions with Framer Motion
  5. 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! :)