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.

ASOasis
7 min read
React TanStack Router Migration Guide: From React Router to Type‑Safe Routing

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:

  1. Introduce TanStack Router at the app root alongside a small pilot area.
  2. Migrate simple pages and links.
  3. Move nested layouts and loaders.
  4. Adopt file-based routing and search-param schemas.
  5. 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 )

  • 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 inside your app to visualize matches, loaders, search state, and navigation. A production-safe variant is available as TanStackRouterDevtoolsInProd. (tanstack.com )

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

  1. Install @tanstack/react-router (+ devtools) and, if applicable, @tanstack/router-plugin.
  2. Wrap your app with using a minimal route tree.
  3. Replace Link/NavLink with TanStack’s and update active styling.
  4. Swap useNavigate and navigate calls, setting from where helpful.
  5. Port loaders to route.loader; add guards with beforeLoad and redirect().
  6. Introduce validateSearch to your most stateful pages.
  7. Move nested layouts; adopt file-based routing to grow coverage.
  8. 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