Routes in React Router v6 are defined with <Route path="..." element={<Component />} /> inside a <Routes> container. The router matches the current URL against all route paths and renders the matching element. Nested routes allow a parent route to render a layout (header, sidebar) while child routes render the page-specific content — the parent uses <Outlet /> to mark where child routes render. Navigation uses <Link> for basic links and <NavLink> for links that need to know if they are the active route.
Basic Route Setup
// src/App.jsx
import { Routes, Route } from "react-router-dom";
import Layout from "@/components/layout/Layout";
import HomePage from "@/pages/HomePage";
import PostDetailPage from "@/pages/PostDetailPage";
import LoginPage from "@/pages/LoginPage";
import RegisterPage from "@/pages/RegisterPage";
import DashboardPage from "@/pages/DashboardPage";
import NotFoundPage from "@/pages/NotFoundPage";
export default function App() {
return (
<Routes>
{/* Root layout wraps all pages */}
<Route path="/" element={<Layout />}>
{/* index route: rendered at exactly "/" */}
<Route index element={<HomePage />} />
{/* URL parameter: :postId is dynamic */}
<Route path="posts/:postId" element={<PostDetailPage />} />
<Route path="posts/by-slug/:slug" element={<PostDetailPage bySlug />} />
{/* Auth routes */}
<Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} />
{/* Dashboard */}
<Route path="dashboard" element={<DashboardPage />} />
{/* 404 — catch-all */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
);
}
Note: The parent
<Route path="/" element={<Layout />}> renders the Layout component for all child routes. The Layout component renders <Outlet /> to mark where child route content appears — the equivalent of {"{{ children }}"} in other frameworks. If a nested route matches, its element renders inside the parent’s <Outlet />. The root <Route index> renders at the exact parent path (no extra path segment).Tip:
NavLink has a special className prop that receives a function with an isActive argument: className={({ isActive }) => isActive ? "font-bold text-blue-600" : "text-gray-600"}. Use NavLink for navigation bars where the active link should look different from inactive ones. Use plain Link for all other internal navigation (post titles, tag links, pagination buttons).Warning: In React Router v6, routes are matched exactly by default — no trailing slash issues and no need for the
exact prop that was required in v5. However, the route order inside <Routes> still matters for ambiguous paths. React Router v6 uses a specificity algorithm (more specific paths win), but for clarity, define specific paths before wildcard paths. Always put path="*" (404) last.Layout with Outlet
// src/components/layout/Layout.jsx
import { Outlet, Link, NavLink } from "react-router-dom";
import { useCurrentUser } from "@/hooks/useCurrentUser";
export default function Layout() {
const { user, isLoggedIn } = useCurrentUser();
return (
<div className="min-h-screen bg-gray-50">
{/* Header — shown on all pages */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-4xl mx-auto px-4 h-16 flex items-center justify-between">
<Link to="/" className="text-xl font-bold text-gray-900">Blog</Link>
<nav className="flex items-center gap-4">
<NavLink
to="/"
end {/* 'end' = only active at exact "/" */}
className={({ isActive }) =>
isActive ? "text-blue-600 font-medium" : "text-gray-600 hover:text-gray-900"
}
>
Home
</NavLink>
{isLoggedIn ? (
<>
<NavLink to="/dashboard" className={({ isActive }) =>
isActive ? "text-blue-600 font-medium" : "text-gray-600"
}>
Dashboard
</NavLink>
<span className="text-sm text-gray-500">{user.name}</span>
</>
) : (
<>
<Link to="/login" className="text-gray-600 hover:text-gray-900">Login</Link>
<Link to="/register" className="bg-blue-600 text-white px-3 py-1 rounded">
Sign Up
</Link>
</>
)}
</nav>
</div>
</header>
{/* Page content — child routes render here */}
<main className="max-w-4xl mx-auto px-4 py-8">
<Outlet />
</main>
<footer className="border-t border-gray-200 py-8 text-center text-gray-400 text-sm">
© 2025 Blog Application
</footer>
</div>
);
}
URL Parameters and Search Params
import { useParams, useSearchParams } from "react-router-dom";
// ── URL parameters: /posts/:postId ────────────────────────────────────────────
function PostDetailPage() {
const { postId } = useParams(); // e.g., "42" for /posts/42
const { post, isLoading } = usePost(Number(postId));
// ...
}
// ── Search parameters: /posts?page=2&tag=python ───────────────────────────────
function HomePage() {
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get("page") ?? 1);
const tag = searchParams.get("tag") ?? null;
function handlePageChange(newPage) {
setSearchParams({ page: newPage, ...(tag ? { tag } : {}) });
}
function handleTagSelect(tagSlug) {
setSearchParams(tagSlug ? { tag: tagSlug, page: 1 } : {});
}
// ...
}
Common Mistakes
Mistake 1 — Not using end on the home NavLink
❌ Wrong — “/” always matches, so home is always highlighted:
<NavLink to="/">Home</NavLink> // active on "/", "/posts/42", "/login"!
✅ Correct — use end to match exactly “/”:
<NavLink to="/" end>Home</NavLink> // ✓ only active at exactly "/"
Mistake 2 — Using React Router v5 patterns in v6
❌ Wrong — v5 API does not work in v6:
<Switch> <Route exact path="/" component={HomePage} /> </Switch>
✅ Correct — v6 API:
<Routes> <Route path="/" element={<HomePage />} /> </Routes> // ✓
Quick Reference
| Component / Hook | Purpose | Example |
|---|---|---|
<Routes> |
Container for route definitions | Wraps all <Route> elements |
<Route> |
Maps path to component | <Route path="posts/:id" element={<Detail />} /> |
<Outlet /> |
Where child routes render | Inside Layout component |
<Link to> |
Internal navigation link | <Link to="/posts/42">Read</Link> |
<NavLink to end> |
Link with active styling | Navigation bar links |
useParams() |
Read URL params | const { postId } = useParams() |
useSearchParams() |
Read/write query string | searchParams.get("page") |