React Router turns a single-page React app into a multi-page experience by mapping URL paths to components. Version 6 (the current release) simplified the API significantly: no more Switch, exact, or render props — just nested <Routes> and <Route> elements.
Basic Setup
npm install react-router-dom
// main.jsx
import { BrowserRouter } from 'react-router-dom'
import App from './App'
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
// App.jsx
import { Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import Users from './pages/Users'
import UserDetail from './pages/UserDetail'
import NotFound from './pages/NotFound'
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
)
}
path="*" is the 404 fallback — it matches anything not caught by earlier routes.
Route Parameters and Query Strings
useParams() extracts dynamic segments from the URL. useSearchParams() reads and updates the query string:
import { useParams, useSearchParams } from 'react-router-dom'
// For /users/42
function UserDetail() {
const { id } = useParams() // id = "42" (always a string)
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser)
}, [id]) // re-fetch if id changes
return user ? <h1>{user.name}</h1> : <p>Loading...</p>
}
// For /search?q=javascript&page=2
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') ?? ''
const page = Number(searchParams.get('page') ?? 1)
const goToPage = (n) => setSearchParams({ q: query, page: n })
return (
<>
<p>Showing page {page} for "{query}"</p>
<button onClick={() => goToPage(page + 1)}>Next page</button>
</>
)
}
Nested Routes with Outlet
Nested routes share a layout. The parent component renders an <Outlet /> where child routes appear:
// App.jsx — route tree
<Routes>
<Route path="/" element={<RootLayout />}>
<Route index element={<Home />} /> {/* matches exactly "/" */}
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="users" element={<UserList />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
// RootLayout.jsx — wraps all routes
function RootLayout() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<main>
<Outlet /> {/* child route renders here */}
</main>
</div>
)
}
// DashboardLayout.jsx — wraps dashboard routes only
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/users">Users</Link>
<Link to="/dashboard/settings">Settings</Link>
</aside>
<section>
<Outlet />
</section>
</div>
)
}
Each <Outlet /> renders the matched child route. This eliminates the need to pass layout components as wrappers around every page component.
Protected Routes
A protected route redirects unauthenticated users to login. In React Router v6, redirect by returning <Navigate>:
// components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '../hooks/useAuth'
function ProtectedRoute({ children, requiredRole }) {
const { user, loading } = useAuth()
const location = useLocation()
if (loading) return <div>Loading...</div>
if (!user) {
// Redirect to login, preserving the current URL so we can come back
return <Navigate to="/login" state={{ from: location }} replace />
}
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />
}
return children
}
// App.jsx
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/admin" element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
} />
The state={{ from: location }} saves where the user was trying to go, so after login you can redirect them back:
// LoginPage.jsx
function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
const redirectTo = location.state?.from?.pathname ?? '/dashboard'
const handleLogin = async (credentials) => {
await login(credentials)
navigate(redirectTo, { replace: true })
}
}
Lazy Loading Routes
Lazy loading splits the bundle so each route’s code is only loaded when navigated to — dramatically reducing initial load time for large apps:
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
const Reports = lazy(() => import('./pages/Reports'))
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} /> {/* always loaded */}
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
)
}
<Suspense> shows the fallback while the lazy component’s chunk is downloading. Place <Suspense> around the entire <Routes> so any route can trigger the fallback.
Programmatic Navigation
useNavigate() lets you navigate in response to events like form submissions, successful logins, or delete confirmations:
import { useNavigate } from 'react-router-dom'
function CreateUserForm() {
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
const user = await createUser(formData)
navigate(`/users/${user.id}`) // go to new user's page
}
const handleCancel = () => navigate(-1) // go back in history
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit">Create</button>
<button type="button" onClick={handleCancel}>Cancel</button>
</form>
)
}
navigate(-1) goes back one step in browser history. navigate('/path', { replace: true }) replaces the current history entry (useful after login so Back doesn’t return to the login page).
NavLink for Active Styling
<NavLink> is like <Link> but adds an active class (or whatever you configure) when the current URL matches:
<NavLink
to="/dashboard"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Dashboard
</NavLink>
// Or with inline styles
<NavLink
to="/settings"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? '#007bff' : 'inherit',
})}
>
Settings
</NavLink>
Summary
- React Router v6:
<Routes>+<Route>replace<Switch>; noexactneeded useParams()for URL parameters (always strings),useSearchParams()for query strings- Nest routes under a layout component that renders
<Outlet />where children appear - Protected routes: check auth in a wrapper component and return
<Navigate>to redirect - Lazy load heavy routes with
lazy()+<Suspense>to reduce initial bundle size useNavigate()for programmatic navigation after events;navigate(-1)for browser back
Comments