ReactDynamic & Nested Routes

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:

TSX
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:

TSX
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>
  )
}
Note
URL params are always strings. If you need a number, convert it: const id = Number(postId). A missing or invalid postId will produce NaN — validate before fetching.
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:

TSX
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:

TSX
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:

TSX
<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

TSX
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 (index prop) 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.

Tip
Name your params clearly. ':id' works but ':postId' or ':userId' is easier to read in useParams() destructuring and avoids confusion when multiple params exist in a nested route.
Warning
Avoid creating very deep route nesting (more than 3 levels). Deep nesting makes Outlet placement confusing and the URL structure hard to share. Flatten if you can — not every related page needs to be a nested child route.