Skip to main content

Nested Routes

Nested routes allow you to create hierarchical route structures where child routes render inside parent route layouts. This is one of the most powerful features of TanStack Router.

Overview

Nested routes enable you to:
  • Share layouts across multiple pages
  • Maintain UI state across navigation
  • Create complex, hierarchical navigation structures
  • Load data at multiple levels of the route tree

Basic Nested Routes

File-Based Routing

With file-based routing, nest routes using directory structure:
src/routes/
  __root.tsx          → Root layout
  index.tsx           → / (home page)
  posts.tsx           → /posts (layout)
  posts/
    index.tsx         → /posts (list)
    $postId.tsx       → /posts/:postId (detail)
src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: RootComponent,
})

function RootComponent() {
  return (
    <>
      <nav className="p-2 flex gap-2">
        <Link to="/" className="[&.active]:font-bold">Home</Link>
        <Link to="/posts" className="[&.active]:font-bold">Posts</Link>
      </nav>
      <hr />
      <Outlet /> {/* Child routes render here */}
    </>
  )
}
src/routes/posts.tsx
import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
import { fetchPosts } from '../api'

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  component: PostsLayout,
})

function PostsLayout() {
  const posts = Route.useLoaderData()
  
  return (
    <div className="flex gap-4 p-4">
      <aside className="w-64">
        <h2>Posts</h2>
        <ul>
          {posts.map((post) => (
            <li key={post.id}>
              <Link
                to="/posts/$postId"
                params={{ postId: post.id }}
                className="hover:underline"
              >
                {post.title}
              </Link>
            </li>
          ))}
        </ul>
      </aside>
      <main className="flex-1">
        <Outlet /> {/* Post detail or index renders here */}
      </main>
    </div>
  )
}
src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/')({
  component: () => <div>Select a post from the sidebar</div>,
})
src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPost } from '../../api'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostDetail,
})

function PostDetail() {
  const post = Route.useLoaderData()
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  )
}

Code-Based Routing

With code-based routing, use getParentRoute and addChildren:
import { createRootRoute, createRoute } from '@tanstack/react-router'

const rootRoute = createRootRoute({
  component: RootComponent,
})

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
  loader: () => fetchPosts(),
  component: PostsLayout,
})

const postsIndexRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '/',
  component: () => <div>Select a post</div>,
})

const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '$postId',
  loader: ({ params }) => fetchPost(params.postId),
  component: PostDetail,
})

const routeTree = rootRoute.addChildren([
  postsRoute.addChildren([
    postsIndexRoute,
    postRoute,
  ]),
])

Pathless Layouts

Pathless layouts wrap child routes without adding a URL segment. This is useful for shared UI like authentication layouts.

File-Based Routing

Use _ prefix for pathless routes:
src/routes/
  _auth.tsx              → Layout (no path)
  _auth/
    dashboard.tsx        → /dashboard
    settings.tsx         → /settings
src/routes/_auth.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { useAuth } from '../auth'

export const Route = createFileRoute('/_auth')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
  component: AuthLayout,
})

function AuthLayout() {
  const auth = useAuth()
  
  return (
    <div className="min-h-screen">
      <header className="border-b p-4">
        <nav className="flex justify-between">
          <div className="flex gap-4">
            <Link to="/dashboard">Dashboard</Link>
            <Link to="/settings">Settings</Link>
          </div>
          <button onClick={() => auth.logout()}>Logout</button>
        </nav>
      </header>
      <main className="p-4">
        <Outlet />
      </main>
    </div>
  )
}
src/routes/_auth/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_auth/dashboard')({
  component: () => <div>Dashboard Content</div>,
})

Code-Based Routing

Use id instead of path:
const authLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: '_auth',
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login' })
    }
  },
  component: AuthLayout,
})

const dashboardRoute = createRoute({
  getParentRoute: () => authLayoutRoute,
  path: '/dashboard',
  component: Dashboard,
})

// Results in: /dashboard (with AuthLayout wrapper)

Multi-Level Nesting

Create deeply nested route hierarchies:
src/routes/
  __root.tsx
  _auth.tsx
  _auth/
    settings.tsx
    settings/
      profile.tsx        → /settings/profile
      security.tsx       → /settings/security
      notifications.tsx  → /settings/notifications
src/routes/_auth/settings.tsx
import { createFileRoute, Link, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_auth/settings')({
  component: SettingsLayout,
})

function SettingsLayout() {
  return (
    <div className="flex gap-6">
      <aside className="w-48">
        <nav className="space-y-2">
          <Link
            to="/settings/profile"
            className="block p-2 hover:bg-gray-100"
            activeProps={{ className: 'bg-blue-100' }}
          >
            Profile
          </Link>
          <Link
            to="/settings/security"
            className="block p-2 hover:bg-gray-100"
            activeProps={{ className: 'bg-blue-100' }}
          >
            Security
          </Link>
          <Link
            to="/settings/notifications"
            className="block p-2 hover:bg-gray-100"
            activeProps={{ className: 'bg-blue-100' }}
          >
            Notifications
          </Link>
        </nav>
      </aside>
      <main className="flex-1">
        <Outlet />
      </main>
    </div>
  )
}

Data Loading in Nested Routes

Each level of nesting can load its own data:
src/routes/_auth.tsx
export const Route = createFileRoute('/_auth')({
  loader: async ({ context }) => {
    // Load user data at the layout level
    const user = await fetchCurrentUser()
    return { user }
  },
  component: AuthLayout,
})
src/routes/_auth/dashboard.tsx
export const Route = createFileRoute('/_auth/dashboard')({
  loader: async ({ context }) => {
    // Load dashboard-specific data
    const stats = await fetchDashboardStats()
    return { stats }
  },
  component: Dashboard,
})

function Dashboard() {
  const { stats } = Route.useLoaderData()
  const { user } = Route.useRouteContext() // From parent route
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <DashboardStats stats={stats} />
    </div>
  )
}

The Outlet Component

The <Outlet /> component renders child routes. Without it, child routes won’t be visible:
import { Outlet } from '@tanstack/react-router'

function Layout() {
  return (
    <div>
      <header>Header</header>
      <Outlet /> {/* Child routes render here */}
      <footer>Footer</footer>
    </div>
  )
}

Parallel Routes

Render multiple route segments at the same level:
const routeTree = rootRoute.addChildren([
  indexRoute,
  aboutRoute,
  contactRoute,
  // All siblings at the same level
])

Best Practices

Keep layouts focused: Each layout should handle one concern (auth, admin, public, etc.)
Don’t forget Outlet: Every parent route that has children must render <Outlet /> or children won’t appear.

Good Nesting Structure

__root.tsx (App shell)
├── _public.tsx (Public layout)
│   ├── index.tsx
│   ├── about.tsx
│   └── contact.tsx
└── _auth.tsx (Auth required)
    ├── dashboard.tsx
    └── settings.tsx
        ├── profile.tsx
        └── security.tsx

Loading States

Show pending UI while nested data loads:
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  pendingComponent: () => <div>Loading post...</div>,
  component: PostDetail,
})

Next Steps

Route Guards

Protect nested routes with authentication

Error Boundaries

Handle errors in nested route hierarchies

Build docs developers (and LLMs) love