Route Configuration and Nested Routes
Route Configuration and Nested Routes
Basic Setup
React Router v6 uses a declarative route configuration with BrowserRouter, Routes, and Route components. The Routes component matches the first Route whose path matches the current URL and renders its element. Nested routes use the Outlet component as a placeholder for child routes, enabling shared layouts.
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Layout() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/products">Products</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<footer>© 2026 My App</footer>
</div>
);
}
Route Groups and Shared Layouts
Route groups let you organize routes without affecting the URL path. Wrap related routes in a group with a shared layout by creating a parent route without a path segment.
<Routes>
<Route element={<AdminLayout />}>
<Route path="/admin/dashboard" element={<AdminDashboard />} />
<Route path="/admin/users" element={<UserManagement />} />
<Route path="/admin/settings" element={<AdminSettings />} />
</Route>
<Route element={<PublicLayout />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
Index Routes and Splats
The index prop on a Route makes it the default child route rendered when the parent path matches exactly. The * catch-all path handles 404 pages for unmatched routes. Splats also enable deep linking into nested content.
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<DashboardNotFound />} />
</Route>
The key concept is that nested Route elements define a tree of URL segments. React Router flattens this tree and matches the most specific path. The Outlet component in a parent element renders the matched child, enabling clean composition of layouts without prop drilling or wrapper components.
Dynamic Parameters and Search Params
Dynamic Parameters and Search Params
Reading Route Parameters
Dynamic segments in a route path are prefixed with : and captured by the useParams hook. Parameters are always strings, so you must parse numeric or boolean values explicitly. Nested dynamic segments enable complex URL patterns like /users/:userId/posts/:postId.
import { useParams, useSearchParams } from 'react-router-dom';
function PostDetail() {
const { userId, postId } = useParams();
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}/posts/${postId}`)
.then(res => res.json())
.then(setPost);
}, [userId, postId]);
if (!post) return <Spinner />;
return (
<article>
<h1>{post.title}</h1>
<p>By user {userId}</p>
<p>{post.body}</p>
</article>
);
}
Managing Search Params
useSearchParams provides a URL-safe way to manage query parameters like filters, pagination, and sorting. It returns a URLSearchParams object and a setter function. Updating search params updates the URL without re-mounting the component.
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const page = parseInt(searchParams.get('page') || '1', 10);
const sort = searchParams.get('sort') || 'name';
const updateFilter = (key, value) => {
setSearchParams(prev => {
if (value) prev.set(key, value);
else prev.delete(key);
return prev;
});
};
return (
<div>
<select value={category} onChange={(e) => updateFilter('category', e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
<select value={sort} onChange={(e) => updateFilter('sort', e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
<p>Page {page} — Category: {category}</p>
<button onClick={() => updateFilter('page', String(page + 1))}>Next</button>
</div>
);
}
Programmatic Navigation
The useNavigate hook enables programmatic navigation from event handlers, effects, or after async operations. Pass a path string or a delta number for relative movement. The second argument supports state that persists across navigation but is not visible in the URL.
function SearchResults({ results }) {
const navigate = useNavigate();
const handleSelect = (item) => {
navigate(`/products/${item.id}`, {
state: { fromSearch: true, query: currentQuery },
});
};
const goBack = () => navigate(-1);
const goForward = () => navigate(1);
return (
<ul>
{results.map(item => (
<li key={item.id} onClick={() => handleSelect(item)}>
{item.name}
</li>
))}
</ul>
);
}
function ProductDetail() {
const location = useLocation();
const fromSearch = location.state?.fromSearch;
return <div>{fromSearch ? 'Came from search' : 'Direct visit'}</div>;
}
Protected Routes and Auth Guards
Protected Routes and Auth Guards
Route Guard Component
A route guard wraps protected content and checks authentication before rendering. If the user is not authenticated, redirect to the login page while preserving the intended destination using location.state.from. The replace prop prevents the login redirect from appearing in browser history.
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children, requiredRole }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <Spinner />;
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/admin" element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
} />
<Route path="/editor" element={
<ProtectedRoute requiredRole="editor">
<EditorPanel />
</ProtectedRoute>
} />
</Routes>
Login with Redirect After Authentication
After successful login, navigate to the page the user originally intended to visit. Read the from location from location.state and fall back to a default route if no state is present. Use replace: true to avoid creating a back-button loop to the login page.
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/dashboard';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(email, password);
navigate(from, { replace: true });
} catch (err) {
setError('Invalid credentials');
}
};
return (
<form onSubmit={handleSubmit}>
<p>You were redirected from {from}</p>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required />
{error && <p className="error">{error}</p>}
<button type="submit">Log In</button>
</form>
);
}
Role-Based Conditional Navigation
Render navigation links conditionally based on user roles to prevent unauthorized users from seeing links they cannot access. This is a UX improvement and does not replace server-side authorization checks.
function Sidebar() {
const { user } = useAuth();
return (
<nav>
<Link to="/dashboard">Dashboard</Link>
{user.role === 'admin' && (
<Link to="/admin">Admin Panel</Link>
)}
{user.role === 'editor' && (
<Link to="/posts/new">New Post</Link>
)}
{user.role === 'admin' && (
<Link to="/admin/users">User Management</Link>
)}
<Link to="/profile">Profile</Link>
</nav>
);
}
Code Splitting and Lazy Loading
Code Splitting and Lazy Loading
React.lazy for Route-Based Splitting
React.lazy dynamically imports a component, splitting it into a separate chunk that loads only when the route is visited. Wrap lazy routes in Suspense with a fallback component. This reduces the initial bundle size significantly for large applications.
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/admin" element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Preloading on Hover
Preload lazy-loaded modules when the user hovers over a link, so the chunk is ready before navigation occurs. This eliminates the perceived loading delay on click. The browser caches the module, so the actual navigation is instant.
function preloadRoute(importFn) {
return () => { importFn(); };
}
const preloadDashboard = preloadRoute(() => import('./pages/Dashboard'));
const preloadSettings = preloadRoute(() => import('./pages/Settings'));
function Sidebar() {
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
>
Dashboard
</Link>
<Link
to="/settings"
onMouseEnter={preloadSettings}
>
Settings
</Link>
</nav>
);
}
Loading States and Error Boundaries
Provide meaningful loading feedback with skeleton components. Wrap lazy-loaded routes in an error boundary to catch and recover from chunk loading failures, which can happen due to network issues or new deployments.
function PageSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
<div className="skeleton-content short" />
</div>
);
}
class RouteErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) {
return (
<div className="error-page">
<h2>Failed to load page</h2>
<p>{this.state.error.message}</p>
<button onClick={() => this.setState({ error: null })}>Retry</button>
</div>
);
}
return this.props.children;
}
}
<BrowserRouter>
<RouteErrorBoundary>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</RouteErrorBoundary>
</BrowserRouter>
A well-structured lazy loading strategy loads only the code needed for the current view, deferring non-critical routes until the user navigates to them. This dramatically improves Time to Interactive for the initial page load.
Quiz
1. In React Router v6, how do you render a child route inside a parent layout component?
2. What happens when you use `useNavigate(-1)` in a component?
3. Why should you use `Navigate` with `state={{ from: location }}` and `replace` when redirecting unauthenticated users?
Flashcards
Question
What is the difference between `Link`, `NavLink`, and `useNavigate` in React Router v6?
Click to reveal answer
Answer
`Link` renders an `<a>` tag that navigates on click without a full page reload. `NavLink` extends `Link` with an `isActive` indicator that lets you style the active route link. `useNavigate` is a hook for programmatic navigation from event handlers or effects — you call `navigate('/path')` instead of rendering a clickable element. Use `Link`/`NavLink` in JSX for user-visible navigation, and `useNavigate` for imperative navigation after form submissions, redirects, or conditional logic.
Question
How do you prevent a `NavLink` from being active on all routes that share a prefix?
Click to reveal answer
Answer
Use the `end` prop on `NavLink`. For example, `<NavLink to="/products" end>` only matches exactly `/products`, not `/products/123` or `/products/edit`. Without `end`, NavLink matches any route starting with the `to` path. This is critical for navigation menus where the parent link should only be active when that specific page is showing, not when a child page is active.
Question
What is the recommended way to handle code-splitting for routes in React Router v6?
Click to reveal answer
Answer
Use `React.lazy(() => import('./Component'))` to dynamically import route components. Wrap the `Routes` in a `Suspense` boundary with a loading fallback (like a skeleton or spinner). For better UX, preload modules on hover using a helper function that calls the import function early. Also wrap lazy routes in an error boundary to gracefully handle chunk loading failures from network issues or new deployments.
Revision Notes
Key Takeaways
- 1. React Router v6 uses declarative route configuration with `Routes` and `Route` components; nested routes render inside `Outlet` in the parent layout
- 2. Dynamic segments (`:param`) are read with `useParams`; search params (`?key=value`) are managed with `useSearchParams` which updates the URL reactively
- 3. Programmatic navigation uses `useNavigate()` — pass a path string or a delta integer for history movement; state passed in the second argument is available via `useLocation().state`
- 4. Protected routes wrap content in a guard component that checks auth state and redirects to login with `Navigate` and `state={{ from: location }}` for post-login redirect
- 5. `NavLink` adds `isActive` styling to links; use the `end` prop to prevent parent routes from matching all child paths
- 6. Lazy loading with `React.lazy` and `Suspense` splits route bundles; preload on hover for instant navigation; wrap in error boundaries for resilience
- 7. Route groups without a path element let you share layouts across related routes without adding URL segments
Interview Tips
- • Explain how React Router v6 route matching works — it matches the most specific path and renders child routes through Outlet
- • Demonstrate a protected route pattern with redirect-after-login, including how `location.state.from` preserves the original destination
- • Describe the difference between `navigate(-1)` and `navigate('/')` — one uses browser history, the other uses a path
- • Walk through implementing lazy loading for routes with error boundaries and explain why preloading on hover improves UX
- • Explain why NavLink needs the `end` prop for exact matching and how the isActive callback works for dynamic class names
Cheat Sheet
BrowserRouter
Routes/Route
Outlet
Link
About — navigates without full page reloadNavLink
<NavLink to="/" end className={({isActive}) => isActive ? 'active' : ''}> — styled active link
useParams
const { id } = useParams(); — read dynamic route segments (:id)
useSearchParams
const [params, setParams] = useSearchParams(); — read/write URL query parameters
useNavigate
const navigate = useNavigate(); navigate('/path', { state, replace }); — programmatic navigation
useLocation
const location = useLocation(); location.pathname, location.state — current URL and state
Navigate
<Navigate to="/login" state={{ from: loc }} replace /> — declarative redirect component
Protected Route
Check auth in wrapper component; return
Lazy Loading
const Comp = lazy(() => import('./Comp')); — wrap in