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)
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 */}
</>
)
}
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
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:
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