Back to Articles
25 min read

Mastering React Router v6: Navigation, Nested Layouts, and Route Guards

Routing is the nervous system of a Single Page Application (SPA). This deep dive covers the transition to React Router v6, mastering dynamic navigation hooks like useNavigate and useParams, handling URL state management, and implementing advanced patterns like code-split routes and authentication guards.

React Router

React Router v6

React Router v6 is a complete rewrite with a smaller bundle, relative routes, automatic route ranking, and hooks-first API; it removes Switch for Routes and simplifies nested routing significantly.

v5 → v6 Changes:
Switch      → Routes
component=  → element=
useHistory  → useNavigate
<Redirect>  → <Navigate>
exact       → (automatic)

BrowserRouter

BrowserRouter uses the HTML5 History API to keep UI in sync with the URL; wrap your app with it at the top level to enable routing throughout your application.

import { BrowserRouter } from 'react-router-dom'; root.render( <BrowserRouter> <App /> </BrowserRouter> );

Routes and Route

Routes is a container that looks through its children Route elements and renders the first one matching the current URL; Route defines a path-to-component mapping.

<Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="*" element={<NotFound />} /> </Routes>

Link renders an anchor tag for navigation without full page reloads; it's the primary way to enable navigation in React Router applications.

import { Link } from 'react-router-dom'; <Link to="/about">About Us</Link> <Link to={{ pathname: '/search', search: '?q=react' }}>Search</Link>

NavLink is a special Link that knows if it's "active"; it automatically applies an active class or style, perfect for navigation menus.

<NavLink to="/about" className={({ isActive }) => isActive ? 'nav-active' : ''} style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })} > About </NavLink>

Navigate is a component that redirects to another route when rendered; it replaces the v5 Redirect component and is useful for conditional redirects in JSX.

import { Navigate } from 'react-router-dom'; if (!user) return <Navigate to="/login" replace />; // 'replace' prevents adding to history stack

useNavigate Hook

useNavigate returns a function to programmatically navigate, replacing v5's useHistory; use it for redirects after form submissions or other imperative navigation needs.

const navigate = useNavigate(); const handleSubmit = () => { saveData(); navigate('/dashboard'); // push navigate('/login', { replace: true }); // replace navigate(-1); // go back };

useParams Hook

useParams returns an object of URL parameters from dynamic route segments; use it to access values defined with :paramName in your route paths.

// Route: <Route path="/users/:userId" element={<User />} /> // URL: /users/123 const { userId } = useParams(); console.log(userId); // "123"

useLocation Hook

useLocation returns the current location object containing pathname, search, hash, and state; useful for analytics, conditional rendering based on URL, or reading passed state.

const location = useLocation(); // location = { pathname: '/about', search: '?ref=home', hash: '#section1', state: { from: 'nav' } } useEffect(() => { analytics.track(location.pathname); }, [location]);

useSearchParams Hook

useSearchParams works like useState but for URL query parameters; it returns the current params and a setter function, keeping URL and state in sync.

const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q'); // ?q=react → "react" setSearchParams({ q: 'vue', page: '2' }); // Updates URL to ?q=vue&page=2

Nested Routes

Nested routes allow child routes to render inside parent layouts; define children routes inside a parent Route, and use Outlet in the parent to render matched children.

<Routes> <Route path="/dashboard" element={<DashboardLayout />}> <Route index element={<Overview />} /> <Route path="settings" element={<Settings />} /> <Route path="profile" element={<Profile />} /> </Route> </Routes>
URL: /dashboard/settings
┌─────────────────────────────┐
│ DashboardLayout             │
│ ┌─────────────────────────┐ │
│ │ <Outlet /> → Settings   │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘

Index Routes

Index routes render at the parent's URL (no additional path segment); they act as the default child route when the parent path matches exactly.

<Route path="/products" element={<ProductsLayout />}> <Route index element={<ProductList />} /> {/* /products */} <Route path=":id" element={<ProductDetail />} /> {/* /products/123 */} </Route>

Route Parameters

Route parameters are dynamic URL segments prefixed with : that capture values from the URL; they're accessed via useParams and enable dynamic routing for items like user profiles or product pages.

<Route path="/posts/:category/:postId" element={<Post />} /> // URL: /posts/tech/42 const { category, postId } = useParams(); // { category: "tech", postId: "42" }

Query Parameters

Query parameters (after ? in URL) are handled separately from route matching using useSearchParams; they're ideal for filters, pagination, and optional search criteria.

// URL: /products?category=shoes&sort=price&page=2 const [params] = useSearchParams(); const category = params.get('category'); // "shoes" const page = parseInt(params.get('page') || '1'); // 2

Protected Routes

Protected routes check authentication/authorization before rendering; implement as a wrapper component that either renders children or redirects to login.

const ProtectedRoute = ({ children }) => { const { user } = useAuth(); if (!user) return <Navigate to="/login" replace />; return children; }; <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />

Lazy Loading Routes

Lazy loading routes uses React.lazy with Suspense to split route components into separate chunks, reducing initial bundle size by loading route code only when navigated to.

const Dashboard = lazy(() => import('./pages/Dashboard')); <Routes> <Suspense fallback={<Loading />}> <Route path="/dashboard" element={<Dashboard />} /> </Suspense> </Routes>

Route-based Code Splitting

Route-based code splitting is the most practical splitting strategy; each route becomes a separate chunk, and combining with lazy loading ensures users download only the code they need.

Initial Bundle: App + Router + Auth (50KB)
                     │
      ┌──────────────┼──────────────┐
      ▼              ▼              ▼
  home.chunk.js  dash.chunk.js  admin.chunk.js
    (30KB)         (80KB)         (120KB)

Outlet Component

Outlet is a placeholder component in parent routes where child route components are rendered; it's the key to creating nested layouts.

function DashboardLayout() { return ( <div> <Sidebar /> <main> <Outlet /> {/* Child routes render here */} </main> </div> ); }

useOutletContext

useOutletContext allows parent routes to pass context data to child routes rendered in an Outlet; it's simpler than Context API for route-specific shared data.

// Parent <Outlet context={{ user, setUser }} /> // Child const { user, setUser } = useOutletContext();

useMatch Hook

useMatch returns match data if the current URL matches the given pattern, or null otherwise; useful for conditional rendering based on route matching without navigating.

const match = useMatch('/users/:id'); // Returns { params: { id: '123' }, pathname: '/users/123', ... } or null if (match) console.log(`Viewing user ${match.params.id}`);

useRoutes Hook

useRoutes lets you define routes as a JavaScript object instead of JSX elements; useful for dynamic route generation or when you prefer configuration-style route definitions.

const routes = useRoutes([ { path: '/', element: <Home /> }, { path: '/about', element: <About /> }, { path: '/users', element: <Users />, children: [ { index: true, element: <UserList /> }, { path: ':id', element: <UserDetail /> } ]} ]); return routes;

Route Transitions

Route transitions animate between route changes using libraries like Framer Motion or React Transition Group; they improve UX but require careful handling of mounting/unmounting timing.

import { motion, AnimatePresence } from 'framer-motion'; <AnimatePresence mode="wait"> <motion.div key={location.pathname} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > <Outlet /> </motion.div> </AnimatePresence>