Dynamic & Nested Routes
Static routes map fixed paths to components. Dynamic routes use URL parameters (:paramName) to capture variable segments — a single route definition handles thousands of different URLs. Nested routes let you build hierarchical UIs where child pages share a parent layout.
URL Parameters with :paramName
Put a colon before a segment name to make it dynamic. The segment captures whatever the user visits at that position:
import { Routes, Route } from 'react-router-dom'
function App() {
return (
<Routes>
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:postId" element={<PostDetail />} />
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
)
}useParams — Reading URL Parameters
useParams() returns an object with all dynamic segments from the current URL. The keys match the :paramName values in the route:
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
function PostDetail() {
const { postId } = useParams() // postId matches ':postId' in the route
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((r) => r.json())
.then((data) => {
setPost(data)
setLoading(false)
})
}, [postId]) // re-fetch when postId changes
if (loading) return <p>Loading post {postId}…</p>
if (!post) return <p>Post not found.</p>
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}Nested Routes with Outlet
Nest Route elements to create hierarchical URLs. The parent renders an Outlet — a placeholder that displays whichever child route currently matches:
import { Routes, Route, Outlet, Link, useParams } from 'react-router-dom'
function App() {
return (
<Routes>
<Route path="/posts" element={<PostsLayout />}>
<Route index element={<PostList />} /> {/* /posts */}
<Route path=":postId" element={<PostDetail />}> {/* /posts/42 */}
<Route index element={<PostBody />} /> {/* /posts/42 */}
<Route path="comments" element={<PostComments />} /> {/* /posts/42/comments */}
</Route>
</Route>
</Routes>
)
}
// Parent layout — renders for ALL /posts/* URLs
function PostsLayout() {
return (
<div style={{ display: 'flex', gap: 24 }}>
<aside>
<h3>Posts</h3>
{/* Sidebar nav here */}
</aside>
<main>
<Outlet /> {/* PostList or PostDetail renders here */}
</main>
</div>
)
}
// Intermediate layout — renders for /posts/:postId and its children
function PostDetail() {
const { postId } = useParams()
return (
<div>
<nav>
<Link to="">Content</Link>
<Link to="comments">Comments</Link>
</nav>
<Outlet /> {/* PostBody or PostComments renders here */}
</div>
)
}Shared Layout Routes
A layout route is a Route with no path that renders a shared UI wrapper (header, sidebar, footer) for a group of child routes. This is the recommended way to avoid duplicating layout code:
function App() {
return (
<Routes>
{/* Public pages — no layout */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
{/* App shell — shared header and sidebar */}
<Route element={<AppShell />}>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
)
}
function AppShell() {
return (
<div>
<Header />
<div style={{ display: 'flex' }}>
<Sidebar />
<main>
<Outlet /> {/* Home, Dashboard, or Settings renders here */}
</main>
</div>
<Footer />
</div>
)
}Optional Params and Catch-All Routes
React Router v6.4+ supports optional segments with a ? suffix. A catch-all * segment matches any remaining path:
<Routes>
{/* Optional lang prefix: matches /en/about AND /about */}
<Route path="/:lang?/about" element={<About />} />
{/* Catch-all: collects everything after /docs/ */}
<Route path="/docs/*" element={<DocsPage />} />
{/* 404 fallback */}
<Route path="*" element={<NotFound />} />
</Routes>
// Inside DocsPage, read the wildcard with useParams:
function DocsPage() {
const { '*': docPath } = useParams()
// For /docs/api/reference, docPath = 'api/reference'
return <p>Doc path: {docPath}</p>
}Full Posts → Detail → Comments Example
import { Link, Outlet, useParams } from 'react-router-dom'
function PostList() {
const posts = [
{ id: 1, title: 'Getting Started with React' },
{ id: 2, title: 'Understanding Hooks' },
{ id: 3, title: 'State Management in 2025' },
]
return (
<div>
<h1>All Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={`${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
function PostDetail() {
const { postId } = useParams()
return (
<div>
<h2>Post {postId}</h2>
<nav style={{ display: 'flex', gap: 12 }}>
<Link to="">Article</Link>
<Link to="comments">Comments</Link>
</nav>
<Outlet />
</div>
)
}
function PostBody() {
const { postId } = useParams()
return <p>Body content for post {postId}.</p>
}
function PostComments() {
const { postId } = useParams()
return <p>Comments for post {postId}.</p>
}Key Rules
Each level of nesting requires an
<Outlet />in the parent to display the active child.Index routes (
indexprop) render when the parent path matches exactly and no child path matches.URL params are always strings — cast to numbers where needed.
Layout routes (no
path) group children under a shared wrapper without adding a URL segment.The
*catch-all matches every path — place it last or it shadows other routes.