Skip to main content
← All Articles Β· Β· 13 min read

TanStack Router: Type-Safe Client-Side Routing for React in 2026

Complete guide to TanStack Router: file-based routing, 100% type-safe search params, route loaders, nested layouts, and when to choose TanStack Router over React Router v7.

tanstack-routerreactroutingtypescripttype-safetyspa

TanStack Router: Type-Safe Client-Side Routing for React in 2026

TanStack Router is the most type-safe routing library available for React. While React Router v7 merged with Remix to become a full-stack framework, TanStack Router went the opposite direction β€” it doubled down on client-side routing with end-to-end TypeScript inference that no other router matches.

If you’re building a data-heavy SPA where type safety across routes, search params, and loaders matters more than SSR, TanStack Router deserves serious consideration.

What Makes TanStack Router Different

The key design decision: everything is inferred from your route definitions. No useParams<{ id: string }>() generics. No casting searchParams.get('page') to a number. The router knows your route tree statically and TypeScript catches mismatches at compile time.

// Wrong param name β†’ TypeScript error
const { postId } = Route.useParams()  // βœ… post.id exists
const { postID } = Route.useParams()  // ❌ TypeScript error

This extends to:

  • Route params
  • Search params (with validation and serialization)
  • Loader data
  • Route context
  • Navigation (navigate, Link)

Installation

npm install @tanstack/react-router
# For file-based routing (recommended)
npm install -D @tanstack/router-plugin @tanstack/router-devtools

Add the Vite plugin:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    TanStackRouterVite(),  // generates routeTree.gen.ts
    react(),
  ],
})

File-Based Routing

TanStack Router uses a file convention similar to Next.js App Router, but generates a type-safe route tree at build time.

File Structure

src/routes/
β”œβ”€β”€ __root.tsx          # Root layout (always rendered)
β”œβ”€β”€ index.tsx           # / route
β”œβ”€β”€ about.tsx           # /about
β”œβ”€β”€ posts/
β”‚   β”œβ”€β”€ index.tsx       # /posts
β”‚   β”œβ”€β”€ $postId.tsx     # /posts/:postId (dynamic segment)
β”‚   └── $postId/
β”‚       └── edit.tsx    # /posts/:postId/edit
└── dashboard/
    β”œβ”€β”€ _layout.tsx     # layout without URL segment
    β”œβ”€β”€ index.tsx       # /dashboard
    └── settings.tsx    # /dashboard/settings

Root Route

// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/posts">Posts</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
})

Index Route

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

export const Route = createFileRoute('/')({
  component: HomePage,
})

function HomePage() {
  return <h1>Welcome</h1>
}

Dynamic Routes with Loaders

// 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),  // params.postId is typed
  component: PostPage,
})

function PostPage() {
  const post = Route.useLoaderData()   // fully typed from loader return
  const { postId } = Route.useParams() // string, inferred from file name

  return (
    <article>
      <h1>{post.title}</h1>
      <p>ID: {postId}</p>
    </article>
  )
}

Type-Safe Search Params

This is where TanStack Router truly shines. Search params are first-class citizens with validation and type inference.

// src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const postSearchSchema = z.object({
  page: z.number().int().min(1).catch(1),
  filter: z.enum(['all', 'published', 'draft']).catch('all'),
  search: z.string().optional(),
})

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

function PostsPage() {
  const { page, filter, search } = Route.useSearch()
  // page: number, filter: 'all' | 'published' | 'draft', search: string | undefined
  // All inferred β€” no casting needed

  const navigate = Route.useNavigate()

  return (
    <div>
      <button onClick={() => navigate({ search: { page: page + 1, filter } })}>
        Next Page
      </button>
    </div>
  )
}

Invalid URL search params are automatically coerced to defaults via .catch() β€” no manual fallback logic needed.

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

// TypeScript validates the `to` path and required params
<Link to="/posts/$postId" params={{ postId: '123' }}>
  View Post
</Link>

// With search params
<Link
  to="/posts/"
  search={{ page: 2, filter: 'published' }}
>
  Published Posts
</Link>

// Active link styling
<Link
  to="/dashboard"
  activeProps={{ className: 'font-bold text-blue-600' }}
  inactiveProps={{ className: 'text-gray-600' }}
>
  Dashboard
</Link>

Programmatic Navigation

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

function SearchBar() {
  const navigate = useNavigate({ from: '/posts/' })

  const handleSearch = (query: string) => {
    navigate({
      search: (prev) => ({ ...prev, search: query, page: 1 }),
    })
  }

  return <input onChange={(e) => handleSearch(e.target.value)} />
}

Route Loaders and Data Loading

TanStack Router’s loader system is synchronous-first (runs before the component renders) with support for async data.

Parallel Loading

// src/routes/dashboard/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchStats, fetchRecentActivity } from '../api'

export const Route = createFileRoute('/dashboard/')({
  loader: async () => {
    // Both requests fire in parallel
    const [stats, activity] = await Promise.all([
      fetchStats(),
      fetchRecentActivity(),
    ])
    return { stats, activity }
  },
  component: DashboardPage,
})

function DashboardPage() {
  const { stats, activity } = Route.useLoaderData()
  // stats and activity are fully typed

  return (
    <div>
      <StatsPanel stats={stats} />
      <ActivityFeed items={activity} />
    </div>
  )
}

Loader with Context

// src/router.tsx β€” inject dependencies into route context
const router = createRouter({
  routeTree,
  context: {
    queryClient,  // TanStack Query client
    auth: undefined as AuthContext | undefined,
  },
})

// src/routes/dashboard/settings.tsx
export const Route = createFileRoute('/dashboard/settings')({
  beforeLoad: ({ context }) => {
    if (!context.auth?.isLoggedIn) {
      throw redirect({ to: '/login' })
    }
  },
  loader: ({ context }) =>
    context.queryClient.ensureQueryData(settingsQuery),
})

Nested Layouts

TanStack Router’s layout system uses _layout files (pathless routes) for shared UI that doesn’t affect the URL:

// src/routes/dashboard/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard/_layout')({
  component: DashboardLayout,
})

function DashboardLayout() {
  return (
    <div className="flex">
      <aside className="w-64">
        <DashboardNav />
      </aside>
      <main className="flex-1">
        <Outlet />
      </main>
    </div>
  )
}

All routes under /dashboard/ render inside DashboardLayout without adding _layout to the URL.

TanStack Router vs React Router v7

FeatureTanStack RouterReact Router v7
TypeScript inferenceEnd-to-end, no castingPartial, generics required
Search param validationBuilt-in (Zod/Valibot)Manual
Primary use caseSPA, data-heavy appsFull-stack apps
SSR supportExperimentalFirst-class
File-based routingYes (Vite plugin)Yes (framework mode)
Nested layoutsYesYes
Route loadersYesYes
Ecosystem maturityGrowing fastVery mature
Bundle size~12 KB~15 KB (router only)

Choose TanStack Router when:

  • Building a SPA with complex navigation state
  • Search params drive significant UI state
  • You want TypeScript to catch routing mistakes at compile time
  • You don’t need SSR

Choose React Router v7 when:

  • You want a full-stack framework (formerly Remix)
  • SSR or edge rendering is required
  • You prefer the established ecosystem and conventions

Setting Up the Router

After file-based routing generates routeTree.gen.ts:

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

export const router = createRouter({
  routeTree,
  defaultPreload: 'intent',   // preload on hover
  defaultStaleTime: 5000,
})

// Register types globally
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
// src/main.tsx
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />
)

DevTools

import { TanStackRouterDevtools } from '@tanstack/router-devtools'

// Add to your root route component
<TanStackRouterDevtools position="bottom-right" />

The devtools panel shows:

  • Current route tree and matched routes
  • Loader data for each route
  • Search param state
  • Navigation history

Summary

TanStack Router offers the best TypeScript experience for client-side routing in 2026. The end-to-end type inference β€” from route definition to useParams(), useSearch(), and useLoaderData() β€” eliminates an entire class of runtime routing bugs.

If you’re building a Vite + React SPA where navigation complexity and type safety matter, TanStack Router is the stronger choice over React Router v7. For full-stack apps that need SSR, React Router v7 (Remix) remains the standard.

Start with the official docs and the Vite quickstart template β€” you’ll have type-safe routes running in under 15 minutes.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.