When I first started building React applications, I thought routing would be simple. Just show different components based on the URL, right? Then I had to implement protected routes, nested layouts, route parameters, and handle 404 pages. That's when I realized that proper routing setup is crucial for building maintainable React applications.
React Router DOM is the de facto standard for routing in React applications. It enables client-side routing, which means navigation happens without full page refreshesβyour app feels fast and responsive. But more importantly, it provides a declarative way to define your application's routes, handle navigation, and protect routes based on authentication status.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What is React Router?
React Router DOM is a routing library for React that provides:
- Client-side routing - Navigation without page refreshes
- Declarative routes - Define routes using JSX
- Nested routes - Organize routes hierarchically
- Protected routes - Control access based on authentication
-
Route parameters - Dynamic routes like
/products/:id - Query parameters - Handle URL search params
- Programmatic navigation - Navigate from code
Installation
Install React Router DOM:
npm install react-router-dom
For TypeScript projects, types are included automatically.
Basic Router Setup
Setting up the router in your main app:
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Provider } from "react-redux";
import { store } from "./state/store";
import Login from "./pages/Auth/Login";
import Signup from "./pages/Auth/Signup";
import DashboardLayout from "./components/DashboardLayout";
import Home from "./pages/Home";
import Products from "./pages/Products/Products";
import AddProduct from "./pages/Products/AddProduct";
import EditProduct from "./pages/Products/EditProduct";
function App() {
return (
<Provider store={store}>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
{/* Protected routes with layout */}
<Route element={<DashboardLayout />}>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/add" element={<AddProduct />} />
<Route path="/products/edit/:id" element={<EditProduct />} />
</Route>
{/* 404 route */}
<Route path="*" element={<div>404 - Page Not Found</div>} />
</Routes>
</BrowserRouter>
</Provider>
);
}
export default App;
Key Components
-
BrowserRouter- Wraps your app and enables routing -
Routes- Container for all route definitions -
Route- Defines a single route with path and element -
Outlet- Renders nested routes (used in layout components)
Using Outlet for Nested Routes
Creating a layout component with Outlet for nested routes:
import { Outlet, useNavigate } from "react-router-dom";
import SideBar from "./SideBar";
import MainContent from "./MainContent";
export default function DashboardLayout() {
const [pageTitle, setPageTitle] = useState("");
const navigate = useNavigate();
const context = {
setPageTitle,
};
return (
<div className="min-h-screen bg-gray-100">
<div className="flex">
<SideBar />
<MainContent>
<Outlet context={context} />
</MainContent>
</div>
</div>
);
}
The Outlet component renders the matched child route. This allows you to create shared layouts (like sidebars and headers) that wrap multiple routes.
Navigation in Components
Using useNavigate Hook
Programmatic navigation using the useNavigate hook:
import { useNavigate, useParams } from "react-router-dom";
function Products() {
const navigate = useNavigate();
const handleAdd = () => {
navigate("/products/add");
};
const handleEdit = (id: string) => {
navigate(`/products/edit/${id}`);
};
const handleBack = () => {
navigate("/products");
};
// Navigate with state
const handleView = (product: Product) => {
navigate("/products/view", { state: { product } });
};
// Navigate with replace (no history entry)
const handleLogin = () => {
navigate("/dashboard", { replace: true });
};
return (
<div>
<button onClick={handleAdd}>Add Product</button>
<button onClick={() => handleEdit("123")}>Edit Product</button>
<button onClick={handleBack}>Back</button>
</div>
);
}
Using Route Parameters
Access route parameters using useParams:
import { useParams } from "react-router-dom";
function EditProduct() {
const { id } = useParams<{ id: string }>();
// Fetch product data using id
const { data: product } = useGetProductQuery(id!);
return (
<div>
<h1>Editing product: {id}</h1>
{/* Edit form */}
</div>
);
}
Using Location State
Access state passed via navigation:
import { useLocation } from "react-router-dom";
function ViewProduct() {
const location = useLocation();
const product = location.state?.product;
return (
<div>
<h1>{product?.name}</h1>
</div>
);
}
Using Link Component
Creating navigation links with Link and NavLink:
import { Link, NavLink } from "react-router-dom";
function SideBar() {
return (
<nav>
{/* Basic Link */}
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
<Link to="/categories">Categories</Link>
{/* NavLink with active styling */}
<NavLink
to="/products"
className={({ isActive }) =>
isActive ? "active-link" : "inactive-link"
}
>
Products
</NavLink>
{/* NavLink with custom active class */}
<NavLink
to="/categories"
className={({ isActive }) =>
`nav-link ${isActive ? "active" : ""}`
}
>
Categories
</NavLink>
</nav>
);
}
Link vs NavLink
-
Link- Basic navigation link -
NavLink- Link with active state styling (useful for navigation menus)
Protected Routes
Implementing route protection for authenticated users:
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
function ProtectedRoute() {
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
// Usage in App.tsx
<Route element={<ProtectedRoute />}>
<Route path="/products" element={<Products />} />
<Route path="/categories" element={<Categories />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route>
Advanced Protected Route
With role-based access control:
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
interface ProtectedRouteProps {
allowedRoles?: string[];
}
function ProtectedRoute({ allowedRoles }: ProtectedRouteProps) {
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
const userRole = useSelector((state: RootState) => state.auth.user?.role);
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (allowedRoles && !allowedRoles.includes(userRole)) {
return <Navigate to="/unauthorized" replace />;
}
return <Outlet />;
}
// Usage
<Route element={<ProtectedRoute allowedRoles={["admin", "manager"]} />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
Route Parameters and Query Strings
Route Parameters
Define dynamic routes with parameters:
// Route definition
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/products/:category/:id" element={<ProductDetail />} />
// Access parameters
function ProductDetail() {
const { id, category } = useParams<{ id: string; category?: string }>();
return <div>Product ID: {id}, Category: {category}</div>;
}
Query Parameters
Handle URL search parameters:
import { useSearchParams } from "react-router-dom";
function Products() {
const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get("page") || "1";
const category = searchParams.get("category") || "";
const handleFilter = (category: string) => {
setSearchParams({ category, page: "1" });
};
return (
<div>
<button onClick={() => handleFilter("electronics")}>
Filter Electronics
</button>
<p>Current page: {page}</p>
<p>Current category: {category}</p>
</div>
);
}
Handling 404 Pages
Create a custom 404 page:
import { Link } from "react-router-dom";
function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-4xl font-bold mb-4">404 - Page Not Found</h1>
<p className="text-gray-600 mb-4">The page you're looking for doesn't exist.</p>
<Link to="/" className="px-4 py-2 bg-blue-500 text-white rounded">
Go Home
</Link>
</div>
);
}
// Usage - must be last route
<Route path="*" element={<NotFound />} />
Nested Routes with Multiple Levels
Create deeply nested routes:
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductsList />} />
<Route path="add" element={<AddProduct />} />
<Route path="edit/:id" element={<EditProduct />} />
</Route>
<Route path="settings" element={<Settings />} />
</Route>
Programmatic Navigation Patterns
Navigate After Action
function AddProduct() {
const navigate = useNavigate();
const [addProduct] = useAddProductMutation();
const handleSubmit = async (data: ProductFormData) => {
try {
await addProduct(data).unwrap();
navigate("/products", { replace: true });
} catch (error) {
console.error("Failed to add product:", error);
}
};
return <ProductForm onSubmit={handleSubmit} />;
}
Navigate with State
function Products() {
const navigate = useNavigate();
const handleView = (product: Product) => {
navigate("/products/view", {
state: { product, returnPath: "/products" }
});
};
return (
<div>
{products.map(product => (
<button key={product.id} onClick={() => handleView(product)}>
View {product.name}
</button>
))}
</div>
);
}
Best Practices
- Use BrowserRouter - For client-side routing in production
- Organize routes by feature - Group related routes together
- Use Outlet for layouts - Create reusable layout components
- Implement protected routes - Secure authenticated pages
- Use NavLink for navigation - Active state styling
- Handle 404 routes - Provide helpful error pages
- Use route parameters - For dynamic routes
- Type your routes - Use TypeScript for type safety
- Lazy load routes - Use React.lazy for code splitting
- Use Navigate for redirects - Declarative redirects
Code Splitting with Lazy Loading
import { lazy, Suspense } from "react";
const Products = lazy(() => import("./pages/Products/Products"));
const Categories = lazy(() => import("./pages/Categories/Categories"));
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/products"
element={
<Suspense fallback={<div>Loading...</div>}>
<Products />
</Suspense>
}
/>
</Routes>
</BrowserRouter>
);
}
Common Patterns
Redirect After Login
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/dashboard";
const handleLogin = async (credentials: LoginData) => {
await login(credentials);
navigate(from, { replace: true });
};
return <LoginForm onSubmit={handleLogin} />;
}
Conditional Navigation
function ProductCard({ product }: { product: Product }) {
const navigate = useNavigate();
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
const handleClick = () => {
if (isAuthenticated) {
navigate(`/products/${product.id}`);
} else {
navigate("/login", { state: { from: `/products/${product.id}` } });
}
};
return <div onClick={handleClick}>{product.name}</div>;
}
Resources and Further Reading
- π Full React Router Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- React Hook Form with Zod - Form validation for login/registration
- Redux Toolkit RTK Query - Data fetching for route components
- TanStack Table Guide - Data tables for route pages
- React Router Documentation - Official React Router docs
- React Router Examples - Official examples
- React TypeScript Guide - TypeScript with React
Conclusion
React Router DOM provides powerful routing capabilities for React applications. With nested routes, protected routes, and programmatic navigation, you can create complex navigation structures for inventory management systems and other React applications.
Key Takeaways:
- React Router DOM enables client-side routing without page refreshes
-
Nested routes with
Outletfor shared layouts - Protected routes for authentication-based access control
-
Route parameters for dynamic routes like
/products/:id -
Programmatic navigation with
useNavigatehook - Link and NavLink for navigation menus
- Query parameters for filtering and search
- 404 handling for better user experience
Whether you're building a simple single-page app or a complex multi-page application, React Router provides the foundation you need. It handles all the routing logic while giving you complete control over navigation and route protection.
What's your experience with React Router? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on React development and frontend development best practices.
Top comments (0)