React TanStack Router Migration Guide: From React Router to Type‑Safe Routing
A practical, step-by-step guide to migrating from React Router to TanStack Router with typed routes, loaders, and schema-validated search.
Image used for representation purposes only.
Why migrate to TanStack Router?
TanStack Router brings end-to-end type safety, first-class URL search-param APIs, parallel route loaders with caching, and ergonomic navigation—without locking you into a framework. If your app has grown beyond “static pages + links,” you’ll likely benefit from its typed route trees, schema-validated search state, and predictable data-loading lifecycle. (tanstack.com )
Compared to a typical React Router setup, TanStack Router emphasizes:
- A typed route tree (code- or file-based) with predictable matching rules.
- Parallel, cache-aware route loaders and redirect hooks.
- JSON-first, schema-validated search params with DX-focused utilities. (tanstack.com )
If you’re on React Router v6 or v7, TanStack’s docs provide a direct migration path and checklist to help you replace imports, concepts, and patterns incrementally. (tanstack.com )
Migration strategies
You can migrate in one go or incrementally. For most teams, a strangler-fig approach is safer:
- Introduce TanStack Router at the app root alongside a small pilot area.
- Migrate simple pages and links.
- Move nested layouts and loaders.
- Adopt file-based routing and search-param schemas.
- Remove React Router once all routes are migrated. (tanstack.com )
Install and scaffold
- Core packages
- npm i @tanstack/react-router
- Optional Devtools: npm i @tanstack/react-router-devtools (tanstack.com )
- File-based routing (recommended)
- For Vite/Rspack/Rsbuild: npm i -D @tanstack/router-plugin, then add the plugin to your bundler config to generate a typed route tree and enable automatic code splitting. (tanstack.com )
From React Router to TanStack Router: mental model map
- Route definition → typed route tree (code-based or file-based). (tanstack.com )
- Outlet and nested routes → identical concept via parent/child routes and
. - Loaders/actions → route.loader and route.beforeLoad, with built-in caching and parallelism. (tanstack.com )
- Navigate/Link → Link and useNavigate with explicit from/to and relative navigation semantics. (tanstack.com )
- Query/search state → JSON-first search params + validateSearch with your schema library. (tanstack.com )
- Redirects/guards → throw redirect() in beforeLoad/loader; dedicated Route.redirect helpers in file routes. (tanstack.com )
- 404 and error boundaries → Not Found errors and error components at the route level. (tanstack.com )
Wire up the router (code-based)
React Router (before):
// main.tsx (React Router v6/7 - simplified)
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter([
{ path: '/', element: <Home/> },
{ path: '/users/:userId', element: <User/> },
])
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
)
TanStack Router (after):
// app.tsx (code-based)
import { createRoot } from 'react-dom/client'
import {
createRootRoute,
createRoute,
createRouter,
RouterProvider,
Outlet,
} from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: () => (
<>
<Header />
<Outlet />
{/* Optional devtools */}
{/* <TanStackRouterDevtools /> */}
</>
),
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: User,
})
const routeTree = rootRoute.addChildren([indexRoute, userRoute])
const router = createRouter({ routeTree })
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
)
This sets up a typed route tree and prepares you for loaders, redirects, and schema-validated search. (tanstack.com )
Optional: switch to file-based routing
With @tanstack/router-plugin, create routes by file path and generate a typed route tree automatically. For example:
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<Header />
<Outlet />
</>
),
})
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
component: User,
})
Add the plugin to Vite/Rspack per docs and enable auto code splitting when desired. (tanstack.com )
Navigation: Link and useNavigate
- Prefer for user-triggered navigation; it preserves href, cmd/ctrl-click, active states, and types checks for params/search. (tanstack.com )
- For imperative flows (e.g., after a mutation), use useNavigate:
import { useNavigate } from '@tanstack/react-router'
function CreatePost() {
const navigate = useNavigate({ from: '/posts' })
return (
<form onSubmit={async (e) => {
e.preventDefault()
const res = await fetch('/api/posts', { method: 'POST' })
const { id } = await res.json()
if (res.ok) navigate({ to: '/posts/$postId', params: { postId: id } })
}} />
)
}
Relative navigation uses from (origin) and to (destination) consistently across APIs. (tanstack.com )
Path params and type-safe hooks
Access params/search/context/loader data with route-bound helpers:
import { getRouteApi } from '@tanstack/react-router'
const userRoute = getRouteApi('/users/$userId')
export function User() {
const { userId } = userRoute.useParams()
// ...
}
getRouteApi returns a typed API for a specific route ID, preventing route/param mix-ups and improving editor autocomplete. (tanstack.com )
Data loading and redirects
Move React Router loaders to route.loader; guard/redirect in route.beforeLoad or loader by throwing redirect():
// src/routes/dashboard/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
if (!context.user) {
throw redirect({ to: '/login' })
}
},
loader: async ({ context }) => {
return context.api.getDashboard()
},
component: Dashboard,
})
Loaders run in parallel across matched routes and can leverage caching to avoid waterfalls; redirects can be thrown from beforeLoad/loader or via Route.redirect in file-based routes. (tanstack.com )
Search params: JSON-first and schema-validated
TanStack Router treats search as structured state by default and lets you validate/transform it per route. Start with validateSearch and your favorite schema library (Zod, Valibot, ArkType, Effect/Schema) for both runtime validation and TypeScript inference:
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'
const productSearch = z.object({
page: z.number().default(1),
q: z.string().default(''),
sort: z.enum(['newest', 'price']).default('newest'),
})
export const Route = createFileRoute('/products')({
validateSearch: zodValidator(productSearch),
component: Products,
})
Now is correctly typed, and reading search in loaders/components is safe and consistent across navigations, refreshes, and deep links. (tanstack.com )
Handling 404s and errors
Use Not Found errors and route-level error components instead of ad-hoc fallbacks. You can define errorComponent per route, and TanStack Router will render it for loader/validation failures as well. (tanstack.com )
Devtools
Install @tanstack/react-router-devtools and render
Common “gotchas” and fixes
- Residual imports: If your UI goes blank or useNavigate complains about context, you likely still import React Router APIs. Uninstall react-router-dom temporarily to surface compiler errors and fix imports. (tanstack.com )
- Relative navigation: Provide from to anchor types and avoid accidental stringly-typed routes. Prefer Link for interactive elements. (tanstack.com )
- Schema defaults vs. catches: Prefer adapters (e.g., zodValidator) to keep link navigation types ergonomic while still providing defaults. (tanstack.com )
Incremental migration checklist
- Install @tanstack/react-router (+ devtools) and, if applicable, @tanstack/router-plugin.
- Wrap your app with
using a minimal route tree. - Replace Link/NavLink with TanStack’s and update active styling.
- Swap useNavigate and navigate calls, setting from where helpful.
- Port loaders to route.loader; add guards with beforeLoad and redirect().
- Introduce validateSearch to your most stateful pages.
- Move nested layouts; adopt file-based routing to grow coverage.
- Remove React Router and clean up unused utilities. (tanstack.com )
Example: end-to-end typed page
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'
const search = z.object({ tab: z.enum(['profile', 'activity']).default('profile') })
export const Route = createFileRoute('/users/$userId')({
validateSearch: zodValidator(search),
loader: async ({ params, context }) => context.api.getUser(params.userId),
component: UserPage,
})
// In a child component file
import { getRouteApi } from '@tanstack/react-router'
const api = getRouteApi('/users/$userId')
export function Tabs() {
const { tab } = api.useSearch()
const navigate = api.useNavigate()
return (
<nav>
<button onClick={() => navigate({ to: '.', search: { tab: 'profile' } })}>Profile</button>
<button onClick={() => navigate({ to: '.', search: { tab: 'activity' } })}>Activity</button>
</nav>
)
}
This page is fully typed from URL params to search state, with navigation constrained to valid values at compile time. (tanstack.com )
Final thoughts
TanStack Router is a pragmatic upgrade for teams who want type-safe routing, fewer data-loading footguns, and URL-centric state that’s actually pleasant to work with. Start by replacing links and a couple of routes, validate your first search schema, then roll forward route by route. Your codebase (and your debugging sessions) will thank you. (tanstack.com )
Related Posts
React Hook Form Validation Tutorial: From Basics to Zod Schemas
Learn React Hook Form validation step by step—from register and Controller to type‑safe Zod schemas, async checks, and real‑world tips.
React Hooks Best Practices: Patterns for Clean, Correct, and Performant Components
A practical guide to React Hooks best practices: state, effects, memoization, refs, custom hooks, concurrency, testing, and pitfalls—with examples.
Build a GraphQL API and React Client: An End‑to‑End Tutorial
Learn GraphQL with React and Apollo Client by building a full stack app with queries, mutations, caching, and pagination—step by step.