
Introduction to React Router
React is primarily designed for building single-page applications (SPAs) where page transitions occur without full page reloads. React Router is the standard routing library for React that enables navigation among views in a React application, allowing developers to build SPAs with multiple views that are bookmarkable and shareable via URLs.
What You’ll Learn in This Chapter
- Core concepts of React Router
- Setting up routes in a React application
- Route parameters and query strings
- Nested routing
- Protected routes
- Code splitting with React Router
- Navigation and redirects
- Best practices for organizing routes
Installing React Router
To get started with React Router, you need to install it via npm or yarn:
# Using npm
npm install react-router-dom
# Using yarn
yarn add react-router-dom
For React Router v6 (the latest major version as of this writing), we’ll be using react-router-dom, which is specifically designed for web applications.
Core Components of React Router
React Router provides several key components that you’ll use frequently:
1. BrowserRouter
BrowserRouter uses the HTML5 history API to keep your UI in sync with the URL. It’s typically used as the root routing component:
import { BrowserRouter } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
{/* The rest of your app goes here */}
</BrowserRouter>
);
}
2. Routes and Route
The Routes component (which replaced Switch in v6) wraps multiple Route components and renders the first one that matches the current URL:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
 return (
  <BrowserRouter>
   <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/dashboard" element={<Dashboard />} />
   </Routes>
  </BrowserRouter>
 );
}
3. Link and NavLink
Link creates navigation links that update the URL without reloading the page:
import { Link } from 'react-router-dom';
function Navigation() {
 return (
  <nav>
   <Link to="/">Home</Link>
   <Link to="/about">About</Link>
   <Link to="/dashboard">Dashboard</Link>
  </nav>
 );
}
NavLink is a special version of Link that knows whether it’s “active” or not:
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? "active-link" : ""}
>
Home
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? "active-link" : ""}
>
About
</NavLink>
</nav>
);
}
4. Navigate
The Navigate component allows for programmatic navigation:
import { Navigate } from 'react-router-dom';
function RedirectComponent() {
 return <Navigate to="/new-page" replace />;
}
URL Parameters
React Router makes it easy to create dynamic routes with parameters:
import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<UserList />} />
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
</BrowserRouter>
);
}
function UserProfile() {
const { userId } = useParams();
return (
<div>
<h2>User Profile</h2>
<p>User ID: {userId}</p>
{/* Fetch and display user data based on userId */}
</div>
);
}
Nested Routes
React Router v6 makes nested routing more intuitive:
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="products" element={<Products />}>
<Route index element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
}
function Layout() {
return (
<div>
<header>
<Navigation />
</header>
<main>
<Outlet /> {/* Child routes will render here */}
</main>
<footer>(c) 2025 My React App</footer>
</div>
);
}
function Products() {
return (
<div>
<h2>Products</h2>
<Outlet /> {/* Nested product routes render here */}
</div>
);
}
The Outlet component acts as a placeholder where child routes will be rendered.
Query Parameters
React Router provides a hook to access query parameters:
import { useSearchParams } from 'react-router-dom';
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const sortBy = searchParams.get('sort') || 'relevance';
// Change the query params programmatically
const changeSorting = (newSortValue) => {
setSearchParams({ q: query, sort: newSortValue });
};
return (
<div>
<h2>Search Results for: {query}</h2>
<div>
<button onClick={() => changeSorting('price')}>Sort by Price</button>
<button onClick={() => changeSorting('name')}>Sort by Name</button>
</div>
{/* Display search results */}
</div>
);
}
Navigation Hooks
React Router provides hooks for programmatic navigation:
javascript
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
// Handle login logic
const success = await attemptLogin();
if (success) {
navigate('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
{/* Login form fields */}
<button type="submit">Login</button>
</form>
);
}
Protected Routes
A common pattern is to protect certain routes based on authentication status:
import { Navigate, Outlet } from 'react-router-dom';
function ProtectedRoute({ isAuthenticated }) {
if (!isAuthenticated) {
// Redirect to login if not authenticated
return <Navigate to="/login" replace />;
}
// Render child routes if authenticated
return <Outlet />;
}
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
return (
<BrowserRouter>
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Protected routes */}
<Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</AuthContext.Provider>
</BrowserRouter>
);
}
Route-Based Code Splitting
To improve performance, you can load components only when needed using React’s lazy loading with React Router:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';
// Lazy load components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Location and History Hooks
React Router provides hooks to access current location and navigate through history:
import { useLocation, useNavigate } from 'react-router-dom';
function LocationAwareComponent() {
const location = useLocation();
const navigate = useNavigate();
return (
<div>
<p>Current pathname: {location.pathname}</p>
<button onClick={() => navigate(-1)}>Go Back</button>
<button onClick={() => navigate(1)}>Go Forward</button>
</div>
);
}
Custom Routes Organization
For larger applications, you might want to organize routes in a more structured way:
// routes.js
import { lazy } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserList = lazy(() => import('./pages/UserList'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));
export const routes = [
{
path: '/',
element: Home,
exact: true,
},
{
path: '/about',
element: About,
},
{
path: '/dashboard',
element: Dashboard,
protected: true,
},
{
path: '/users',
element: UserList,
protected: true,
},
{
path: '/users/:userId',
element: UserProfile,
protected: true,
},
{
path: '*',
element: NotFound,
},
];
// App.js
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Suspense } from 'react';import { routes } from './routes';
function App() {
const isAuthenticated = useAuth();
// Your authentication hook
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{
routes.map((route) => {
const Element = route.element;
// Handle protected routes
if (route.protected && !isAuthenticated) {
return (
<Route key={route.path} path={route.path} element={<Navigate to="/login" replace />} />
);
}
return (
<Route key={route.path} path={route.path} element={<Element /> exact={route.exact} />
);
}
)
}
</Routes>
</Suspense>
</BrowserRouter>
);
}
Complete Example: Building a Multi-Page Application
Let’s put everything together to build a simple multi-page application with React Router:
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Dashboard from './pages/Dashboard';
import NotFound from './pages/NotFound';
import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider, useAuth } from './contexts/AuthContext';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
{/* Public routes */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />} />
<Route path="products/:productId" element={<ProductDetail />} />
{/* Protected routes */}
<Route element={<ProtectedRoute />}>
<Route path="dashboard" element={<Dashboard />} />
</Route>
{/* Catch-all route */}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
// components/Layout.js
import { Outlet } from 'react-router-dom';
import Navigation from './Navigation';
function Layout() {
return (
<div>
<header>
<Navigation />
</header>
<main>
<Outlet />
</main>
<footer>
<p>(c) 2025 My React Router App</p>
</footer>
</div>
);
}
export default Layout;
// components/Navigation.js
import { NavLink } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
function Navigation() {
const { isAuthenticated, logout } = useAuth();
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? "active" : ""}
>
Home
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? "active" : ""}
>
About
</NavLink>
<NavLink
to="/products"
className={({ isActive }) => isActive ? "active" : ""}
>
Products
</NavLink>
{isAuthenticated ? (
<>
<NavLink
to="/dashboard"
className={({ isActive }) => isActive ? "active" : ""}
>
Dashboard
</NavLink>
<button onClick={logout}>Logout</button>
</>
) : (
<NavLink
to="/login"
className={({ isActive }) => isActive ? "active" : ""}
>
Login
</NavLink>
)}
</nav>
);
}
export default Navigation;
// components/ProtectedRoute.js
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
function ProtectedRoute() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
export default ProtectedRoute;
// contexts/AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => setIsAuthenticated(true);
const logout = () => setIsAuthenticated(false);
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
Common Issues and Solutions
1. Handling 404 Pages
React Router can handle unknown routes with a catch-all route:
<Route path="*" element={<NotFound />} />
2. Scroll Restoration
When navigating between pages, the scroll position doesn’t automatically reset. You can create a component to handle this:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
// Use it in your App
function App() {
return (
<BrowserRouter>
<ScrollToTop />
{/* rest of your app */}
</BrowserRouter>
);
}
3. Server Configuration for SPAs
For SPAs to work correctly, the server needs to be configured to redirect all requests to index.html. Here are some common server configurations:
For Apache (.htaccess):
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
For Nginx:
location / {
 try_files $uri $uri/ /index.html;
}
Best Practices for React Router
- Always use the latest version: Keep up with the latest React Router updates for best performance and features.
- Organize routes efficiently: For larger applications, consider organizing routes in separate files.
- Implement code splitting: Use React’s lazy loading to improve initial load time.
- Handle loading states: Show loading indicators when routes are loading.
- Set up proper error boundaries: Handle errors gracefully in your routing.
- Use the ‘exact’ prop when needed: In React Router v5, use exact to match paths exactly. In v6, the exact behavior is default.
- Follow the DRY principle: Don’t repeat route patterns; abstract them into configuration objects.
- Provide meaningful 404 pages: Always have a catch-all route for unknown paths.
- Manage browser history properly: Be cautious with history manipulation to avoid breaking the back button.
- Test your routes: Write tests for your routing logic to ensure everything works as expected.
Conclusion
React Router provides a powerful and flexible solution for handling navigation in React applications. By understanding its core concepts and components, you can create complex routing patterns that enhance the user experience while maintaining clean, maintainable code.
In this chapter, we’ve covered:
- Basic route setup and configuration
- Dynamic routes with parameters
- Nested routing
- Protected routes
- Programmatic navigation
- Code splitting with routes
- Best practices for React Router
In the next chapter, we’ll explore optimization and performance techniques to make your React applications faster and more efficient.
Practice Exercise
Create a simple React application with the following routes:
- A home page (/)
- An about page (/about)
- A product listing page (/products)
- A product detail page (/products/:productId)
- A protected dashboard page (/dashboard) that requires authentication
- A 404 page for unknown routes
Include proper navigation between these pages and implement protected routes for the dashboard.