| | |

TanStack Router 04 ⚛️ Route Trees — Building type-safe trees

⏱️ Estimated read: 11 min | |

The previous chapter covered the file conventions that map a directory structure to URLs. This chapter covers the route tree itself: the nested data structure that the router uses to match URLs and the generated types that make navigation type-safe. A route tree is not a flat list. It is a hierarchy of routes, where each route can have children, and the children render inside the parent’s <Outlet />. The @tanstack/router-plugin reads the src/routes/ directory and generates the routeTree.gen.ts file, which is a nested object that the createRouter function consumes. The critical detail is that the generated tree is the source of truth for the type system. The Link component’s to prop, the useParams hook’s return type, and the useSearch hook’s type are all inferred from the route tree. Without the tree, there is no type safety.

Key point: The route tree is built by nesting routes with addChildren. The file-based routing generates the tree automatically. The createRouter({ routeTree }) connects the tree to the router. The declare module '@tanstack/react-router' { interface Register { router: typeof router } } block registers the tree’s types globally, which enables the type-safe navigation and hooks. Without the registration, the to prop is a generic string, and the hooks return union types.


Why route trees require a specific mental model

The nested structure problem. A route tree is not a flat array. The /posts/$postId/edit route is not a top-level route. It is a child of /posts/$postId, which is a child of /posts, which is a child of the root route. The nesting is what allows the layout routes to wrap their children. The posts.tsx layout renders an <Outlet />, and the posts.$postId.tsx route renders inside it. The tree structure mirrors the URL structure, and the component tree mirrors the route tree.

The type inference problem. The router’s type system is driven by the route tree. The RoutePaths type is a union of all valid paths in the tree. The useParams hook returns the params for the specific route, but only if the route is registered. Without the declare module block, the to prop accepts any string, and a typo in the path is a runtime error instead of a compile-time error. The registration is the connection between the generated tree and the TypeScript compiler.

The code-based versus file-based problem. The file-based routing generates the tree automatically. The code-based routing builds the tree manually with createRoute and addChildren. The code-based approach is more explicit but requires more code. The file-based approach is the recommended default because the tree is always in sync with the files. The addChildren method accepts an array or an object. The object syntax is faster for TypeScript inference in large trees, but the file-based routing handles this automatically.

The lazy loading problem. In large applications, the route tree can be large, and the TypeScript inference can be slow. The getRouteApi function is the mechanism for accessing the route’s typed hooks without importing the full route configuration into a lazy chunk. The createLazyFileRoute is the lazy variant of createFileRoute. The tree is still the source of truth, but the type access is scoped to the route.

The trade-off. The file-based routing is the recommended path because the tree is generated. The code-based routing is the fallback for the applications that need programmatic route construction. The type safety is the differentiator. A router without types is a router that fails at runtime. A router with types is a router that fails at compile time.


a. The generated route tree and the registration

The routeTree.gen.ts file is generated by the plugin. It imports the route files and builds the nested tree.

// src/routeTree.gen.ts (generated, simplified)
import { Route as rootRoute } from './routes/__root'
import { Route as aboutRoute } from './routes/about'
import { Route as postsRoute } from './routes/posts'
import { Route as postsPostIdRoute } from './routes/posts.$postId'

const routeTree = rootRoute.addChildren([
  aboutRoute,
  postsRoute.addChildren([postsPostIdRoute]),
])

export { routeTree }

The tree is a nested structure. The postsRoute has the postsPostIdRoute as a child. The aboutRoute is a sibling of the postsRoute. The rootRoute is the parent of both. The addChildren method builds the hierarchy.

The createRouter function consumes the tree. The declare module block registers the router type globally.

// src/main.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

The registration is the mechanism that enables the type-safe Link, useParams, and useNavigate. Without it, the to prop is a string, and the hooks return unknown. With it, the to prop is a union of the valid paths, and the hooks return the typed params.


b. The Route.useParams and the typed access

The Route.useParams() method is the typed access to the params. It is available on the route object returned by createFileRoute.

// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  // postId is typed as string
  return <div>Post: {postId}</div>
}

The Route.useParams() is the preferred pattern when the component is in the same file as the route definition. The from option is inferred automatically. The useParams({ from: '/posts/$postId' }) is the equivalent for the components that are in a different file.

The params can be parsed to a different type. The params.parse option in the route definition transforms the string params to the desired type.

export const Route = createFileRoute('/posts/$postId')({
  params: {
    parse: (params) => ({ postId: Number(params.postId) }),
    stringify: (params) => ({ postId: String(params.postId) }),
  },
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  // postId is typed as number
  return <div>Post: {postId}</div>
}

The parse function is called when the route matches. The stringify function is called when the route is navigated to. The type system tracks the transformation, so the postId is a number in the component.


c. The type-safe navigation and the from option

The Link component’s to prop is typed against the route tree. The params prop is required for the dynamic segments.

import { Link } from '@tanstack/react-router'

<Link to="/posts/$postId" params={{ postId: '123' }}>
  Post 123
</Link>

// TypeScript error: missing params
<Link to="/posts/$postId">Post</Link>

// TypeScript error: invalid path
<Link to="/invalid">Invalid</Link>

The to prop is a union of the valid paths. The params prop is required when the path has a dynamic segment. The search prop is typed against the validateSearch schema.

The useNavigate hook is the imperative version. The to and params are typed the same way.

const navigate = useNavigate()

navigate({ to: '/posts/$postId', params: { postId: '123' } })

// TypeScript error: missing params
navigate({ to: '/posts/$postId' })

The useSearch hook returns the typed search params. The from option narrows the type to the specific route.

// Without from: union of all search params
const search = useSearch()

// With from: specific to /posts
const search = useSearch({ from: '/posts' })

The from option is the mechanism for narrowing the type. Without it, the type is a union of all routes’ search params, which is less precise and slower to check.


Complete Example Session

# ============================================
# PART 1: GENERATED TREE
# ============================================

// src/routeTree.gen.ts
import { Route as rootRoute } from './routes/__root'
import { Route as aboutRoute } from './routes/about'
import { Route as postsRoute } from './routes/posts'
import { Route as postsPostIdRoute } from './routes/posts.$postId'

const routeTree = rootRoute.addChildren([
  aboutRoute,
  postsRoute.addChildren([postsPostIdRoute]),
])
export { routeTree }
# ============================================
# PART 2: REGISTER THE ROUTER
# ============================================

import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
# ============================================
# PART 3: TYPED PARAMS
# ============================================

// src/routes/posts.$postId.tsx
const { postId } = Route.useParams()
// postId: string
# ============================================
# PART 4: PARSED PARAMS
# ============================================

export const Route = createFileRoute('/posts/$postId')({
  params: {
    parse: (params) => ({ postId: Number(params.postId) }),
    stringify: (params) => ({ postId: String(params.postId) }),
  },
  component: PostComponent,
})

const { postId } = Route.useParams()
// postId: number
# ============================================
# PART 5: TYPED LINK
# ============================================

<Link to="/posts/$postId" params={{ postId: '123' }}>Post</Link>
# ============================================
# PART 6: TYPE ERROR ON MISSING PARAMS
# ============================================

<Link to="/posts/$postId">Post</Link>
// TypeScript error: missing params
# ============================================
# PART 7: TYPED SEARCH
# ============================================

import { z } from 'zod'

const searchSchema = z.object({
  page: z.number().default(1),
})

export const Route = createFileRoute('/posts')({
  validateSearch: searchSchema,
  component: PostsComponent,
})

const { page } = Route.useSearch()
// page: number
# ============================================
# PART 8: NARROWED SEARCH
# ============================================

const search = useSearch({ from: '/posts' })
// search is typed for /posts only
# ============================================
# PART 9: CODE-BASED TREE
# ============================================

const rootRoute = createRootRoute()
const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/posts',
})
const routeTree = rootRoute.addChildren([postsRoute])

The nine parts trace the route tree: generated tree, registration, typed params, parsed params, typed link, type error, typed search, narrowed search, and code-based tree.


Quick Reference

Tree Building

MethodPurpose
addChildrenAdd child routes
getParentRouteLink to parent (code-based)
routeTree.gen.tsGenerated tree (file-based)

Type Registration

BlockPurpose
declare module '@tanstack/react-router'Global type registration
interface Register { router: typeof router }Router type

Typed Hooks

HookType Source
Route.useParams()Route path params
Route.useSearch()validateSearch schema
Route.useLoaderData()loader return type
useParams({ from })Specific route
useSearch({ from })Specific route

Path Params

OptionPurpose
params.parseString to type
params.stringifyType to string

Best Practices

✅ Do This:

declare module '@tanstack/react-router' {
  interface Register { router: typeof router }
}
<!-- Register the router for type safety -->          # ✅
const { postId } = Route.useParams()
<!-- Use Route.useParams in the route file -->       # ✅
params: { parse: (p) => ({ id: Number(p.id) }) }
<!-- Parse params to the correct type -->            # ✅

❌ Don’t Do This:

# Skip the registration
<!-- Link and hooks lose type safety -->             # ❌
# Use useParams() without from
<!-- Returns union of all routes' params -->         # ❌
# Assume params are numbers
<!-- They are strings from the URL -->               # ❌

Common Pitfalls

PitfallWhy It HappensFix
to accepts any stringNo registrationAdd declare module
postId type is unknownNo from or wrong routeUse Route.useParams
Params are stringsNo parseAdd params.parse
Search type is unionNo fromUse useSearch({ from })
Type inference slowLarge tree, no object syntaxUse object addChildren

Real-World Examples

1. Generated Tree

import { routeTree } from './routeTree.gen'

2. Register Router

declare module '@tanstack/react-router' { interface Register { router: typeof router } }

3. Typed Params

const { postId } = Route.useParams()

4. Parsed Params

params: { parse: (p) => ({ id: Number(p.id) }) }

5. Typed Link

<Link to="/posts/$postId" params={{ postId: '1' }}>Post</Link>

6. Typed Search

const { page } = Route.useSearch()

7. Narrowed Search

const search = useSearch({ from: '/posts' })

8. Code-Based Tree

const routeTree = rootRoute.addChildren([postsRoute])

9. Object Syntax

rootRoute.addChildren({ postsRoute: postsRoute.addChildren({ postRoute }) })

10. Lazy Route

const routeApi = getRouteApi('/posts')
const { page } = routeApi.useSearch()

Visual

Route Tree Structure

┌──────────────────────────────────────────────┐
│  rootRoute                                   │
│  ├── indexRoute (/)                          │
│  ├── aboutRoute (/about)                     │
│  └── postsRoute (/posts)                     │
│      └── postsPostIdRoute (/posts/$postId)   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  routeTree.gen.ts                            │
│  → rootRoute.addChildren([...])              │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  createRouter({ routeTree })                 │
└──────────────────────────────────────────────┘

Type Registration Flow

┌──────────────────────────────────────────────┐
│  const router = createRouter({ routeTree })  │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  declare module { Register { router } }      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  <Link to="/posts/$postId" params={{...}}>   │
│  → TypeScript validates the path and params  │
└──────────────────────────────────────────────┘

Summary

ItemValue
Tree generationrouteTree.gen.ts (file-based)
Tree buildingaddChildren (code-based)
Router creationcreateRouter({ routeTree })
Type registrationdeclare module block
Params hookRoute.useParams()
Search hookRoute.useSearch()
Loader hookRoute.useLoaderData()
Params parsingparams.parse / params.stringify
Narrowed searchuseSearch({ from })

Key takeaways:

  • The route tree is the connection between the files and the router. The file-based routing generates the tree automatically. The routeTree.gen.ts file is the nested structure that the createRouter function consumes.
  • The type registration is mandatory for type safety. The declare module '@tanstack/react-router' { interface Register { router: typeof router } } block registers the tree’s types globally. Without it, the to prop is a string, and the hooks return union types.
  • The Route.useParams() is the typed access. It returns the params for the specific route. The from option is required when the component is in a different file. The params.parse option transforms the string params to the desired type.
  • The useSearch({ from }) narrows the search type. Without the from option, the type is a union of all routes’ search params. With it, the type is specific to the route.
  • The code-based tree is the manual alternative. The createRoute and addChildren methods build the tree. The object syntax is faster for TypeScript inference in large trees.

Remember: The route tree is the hierarchy. The addChildren is the builder. The routeTree.gen.ts is the generated file. The createRouter is the consumer. The declare module is the type bridge. The Route.useParams is the typed access. The from is the narrowing. The exam tests the tree structure and the type registration.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!