Routing in React: A Complete Guide to React Router v6
Erik Nguyen / December 21, 2024
Routing in React: A Complete Guide to React Router v6
Navigation is a fundamental aspect of any web application. React Router v6 provides a powerful and intuitive way to handle routing in React applications. Let's explore how to create seamless navigation experiences for your users, starting from the basics and moving to advanced concepts.
Understanding the Fundamentals
React Router v6 brought significant changes in how we handle routing compared to previous versions. The new API is more declarative and easier to understand, while providing powerful features for complex routing scenarios.
First, let's set up React Router in our application:
# Install React Router
npm install react-router-dom@6
Basic Route Setup
Let's start with a simple example that demonstrates the basic concepts:
import {
BrowserRouter,
Routes,
Route,
Link
} from 'react-router-dom';
// Our page components
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
{/* Navigation menu */}
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/products">Products</Link>
</nav>
{/* Route definitions */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
In this example, we've created a basic navigation structure. The BrowserRouter
component provides the routing context, while Routes
and Route
components define our application's routing rules. The Link
component creates navigation links that won't trigger a full page reload.
Nested Routes and Layouts
React Router v6 makes it easy to create nested routes and shared layouts. This is particularly useful for creating complex navigation structures:
import { Outlet } from 'react-router-dom';
// Layout component with shared UI elements
function DashboardLayout() {
return (
<div className="dashboard-layout">
<nav className="dashboard-nav">
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
<Link to="notifications">Notifications</Link>
</nav>
{/* Outlet renders the child route components */}
<main className="dashboard-content">
<Outlet />
</main>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
{/* Nested routes within dashboard */}
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
<Route path="notifications" element={<Notifications />} />
</Route>
</Routes>
</BrowserRouter>
);
}
The Outlet
component is a key concept in React Router v6. It acts as a placeholder where child routes will be rendered, allowing you to create consistent layouts with shared UI elements.
Dynamic Routes and Parameters
Real-world applications often need to handle dynamic routes with parameters. Here's how to implement them:
// Route definition with URL parameters
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="products" element={<Products />}>
<Route path=":productId" element={<ProductDetails />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Component using URL parameters
function ProductDetails() {
const { productId } = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
// Fetch product details using the productId
async function fetchProduct() {
try {
const data = await fetchProductDetails(productId);
setProduct(data);
} catch (error) {
console.error('Failed to fetch product:', error);
}
}
fetchProduct();
}, [productId]);
if (!product) return <div>Loading...</div>;
return (
<div className="product-details">
<h1>{product.name}</h1>
<p>{product.description}</p>
<price>${product.price}</price>
</div>
);
}
Protected Routes and Authentication
A common requirement is protecting certain routes based on authentication status. Here's how to implement protected routes:
import { Navigate, useLocation } from 'react-router-dom';
// Auth context for managing authentication state
const AuthContext = createContext(null);
function ProtectedRoute({ children }) {
const { user } = useContext(AuthContext);
const location = useLocation();
if (!user) {
// Redirect to login while saving the attempted URL
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="login" element={<Login />} />
{/* Protected dashboard routes */}
<Route
path="dashboard"
element={
<ProtectedRoute>
<DashboardLayout />
</ProtectedRoute>
}
>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
// Login component with redirect after authentication
function Login() {
const { login } = useContext(AuthContext);
const location = useLocation();
const navigate = useNavigate();
const handleLogin = async (credentials) => {
try {
await login(credentials);
// Redirect to the page they tried to visit or dashboard
const destination = location.state?.from?.pathname || '/dashboard';
navigate(destination, { replace: true });
} catch (error) {
console.error('Login failed:', error);
}
};
return (
<form onSubmit={handleLogin}>
{/* Login form fields */}
</form>
);
}
Advanced Navigation Features
React Router v6 provides several advanced features for complex navigation scenarios:
1. Programmatic Navigation
function ProductCard({ product }) {
const navigate = useNavigate();
const handleClick = () => {
// Navigate programmatically with state
navigate(`/products/${product.id}`, {
state: { productData: product }
});
};
return (
<div onClick={handleClick}>
<h3>{product.name}</h3>
<p>{product.shortDescription}</p>
</div>
);
}
2. Route Loaders and Actions
React Router v6.4+ introduced data loading and mutation capabilities:
// Route configuration with loader and action
const router = createBrowserRouter([
{
path: "/products",
element: <Products />,
loader: async () => {
const products = await fetchProducts();
return { products };
},
action: async ({ request }) => {
const formData = await request.formData();
return createProduct(Object.fromEntries(formData));
}
}
]);
// Component using loader data
function Products() {
const { products } = useLoaderData();
const { state } = useNavigation();
if (state === 'loading') return <LoadingSpinner />;
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
3. Error Boundaries
Handle routing errors gracefully:
function ErrorBoundary() {
const error = useRouteError();
return (
<div className="error-container">
<h1>Oops! Something went wrong</h1>
<p>{error.message}</p>
<Link to="/">Return to Home</Link>
</div>
);
}
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorBoundary />,
children: [
// ... other routes
]
}
]);
Best Practices and Performance Tips
- Lazy Loading Routes
import { lazy, Suspense } from 'react';
// Lazy load route components
const ProductDetails = lazy(() => import('./pages/ProductDetails'));
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="products/:id"
element={
<Suspense fallback={<LoadingSpinner />}>
<ProductDetails />
</Suspense>
}
/>
</Routes>
</BrowserRouter>
);
}
- Scroll Management
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
function App() {
return (
<BrowserRouter>
<ScrollToTop />
<Routes>{/* ... */}</Routes>
</BrowserRouter>
);
}
Common Patterns and Solutions
1. Breadcrumb Navigation
function Breadcrumbs() {
const location = useLocation();
const paths = location.pathname.split('/')
.filter(Boolean);
return (
<nav aria-label="breadcrumb">
<ol className="breadcrumb">
<li>
<Link to="/">Home</Link>
</li>
{paths.map((path, index) => {
const routeTo = `/${paths.slice(0, index + 1).join('/')}`;
const isLast = index === paths.length - 1;
return (
<li key={routeTo}>
{isLast ? (
<span>{path}</span>
) : (
<Link to={routeTo}>{path}</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}
2. Modal Routes
function ProductsLayout() {
return (
<div className="products-page">
<h1>Products</h1>
<div className="products-grid">
<Outlet />
</div>
{/* Modal route outlet */}
<div className="modal-container">
<Outlet context="modal" />
</div>
</div>
);
}
// Route configuration
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductsList />} />
<Route
path=":id"
element={<ProductModal />}
/>
</Route>
Conclusion
React Router v6 provides a powerful and flexible routing solution for React applications. By understanding its core concepts and features, you can create intuitive navigation experiences that scale with your application's complexity.
Remember these key points:
- Use nested routes for complex layouts
- Implement protected routes for authentication
- Leverage dynamic parameters for flexible routing
- Consider code splitting for better performance
- Handle errors gracefully with error boundaries
Happy routing! 🚀