This guide provides a step-by-step process to migrate your application from React Router v7 to TanStack Router. We'll cover the complete migration process from removing React Router dependencies to implementing TanStack Router's type-safe routing patterns.
Time Required: 2-4 hours depending on app complexity
Difficulty: Intermediate
Prerequisites: Basic React knowledge, existing React Router v7 app
Before making any changes, prepare your environment and codebase:
1.1 Create a backup branch
git checkout -b migrate-to-tanstack-router
git push -u origin migrate-to-tanstack-router
1.2 Install TanStack Router (keep React Router temporarily)
# Install TanStack Router
npm install @tanstack/react-router
# Install development dependencies
npm install -D @tanstack/router-plugin @tanstack/react-router-devtools
1.3 Set up the router plugin for your bundler
For Vite users, update your vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter(), // Add this before react plugin
react(),
],
})
For other bundlers, see our bundler configuration guides.
2.1 Create router configuration file
Create tsr.config.json in your project root:
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"quoteStyle": "single"
}
2.2 Create routes directory
mkdir src/routes
3.1 Identify your current React Router v7 setup
React Router v7 introduced several new patterns. Look for:
3.2 Create root route
Create src/routes/__root.tsx:
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
{/* Your existing layout/navbar content */}
<div className="p-2 flex gap-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
</div>
<hr />
<Outlet />
<TanStackRouterDevtools />
</>
),
})
3.3 Create index route
Create src/routes/index.tsx for your home page:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: Index,
})
function Index() {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
)
}
3.4 Convert React Router v7 loaders
React Router v7 simplified loader patterns. Here's how to migrate them:
React Router v7:
// app/routes/posts.tsx
export async function loader() {
const posts = await fetchPosts()
return { posts } // v7 removed need for json() wrapper
}
export default function Posts() {
const { posts } = useLoaderData()
return <div>{/* render posts */}</div>
}
TanStack Router equivalent: Create src/routes/posts.tsx:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts()
return { posts }
},
component: Posts,
})
function Posts() {
const { posts } = Route.useLoaderData()
return <div>{/* render posts */}</div>
}
3.5 Convert dynamic routes
React Router v7:
// app/routes/posts.$postId.tsx
export async function loader({ params }) {
const post = await fetchPost(params.postId)
return { post }
}
export default function Post() {
const { post } = useLoaderData()
return <div>{post.title}</div>
}
TanStack Router equivalent: Create src/routes/posts/$postId.tsx:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
component: Post,
})
function Post() {
const { post } = Route.useLoaderData()
const { postId } = Route.useParams()
return <div>{post.title}</div>
}
3.6 Convert React Router v7 actions
React Router v7:
export async function action({ request, params }) {
const formData = await request.formData()
const result = await updatePost(params.postId, formData)
return { success: true }
}
TanStack Router equivalent:
export const Route = createFileRoute('/posts/$postId/edit')({
component: EditPost,
// Actions are typically handled differently in TanStack Router
// Use mutations or form libraries like React Hook Form
})
function EditPost() {
const navigate = useNavigate()
const handleSubmit = async (formData) => {
const result = await updatePost(params.postId, formData)
navigate({ to: '/posts/$postId', params: { postId } })
}
return <form onSubmit={handleSubmit}>{/* form */}</form>
}
4.1 Server-Side Rendering Migration
React Router v7 introduced framework mode with SSR. If you're using this:
React Router v7 Framework Mode:
// react-router.config.ts
export default {
ssr: true,
prerender: ['/'],
}
TanStack Router approach:
TanStack Router has built-in SSR capabilities. Set up your router for SSR:
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
// Add any SSR context here
},
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export { router }
For server-side rendering, use TanStack Router's built-in SSR APIs:
// server.tsx
import { createMemoryHistory } from '@tanstack/react-router'
import { StartServer } from '@tanstack/start/server'
export async function render(url: string) {
const router = createRouter({
routeTree,
history: createMemoryHistory({ initialEntries: [url] }),
})
await router.load()
return (
<StartServer router={router} />
)
}
4.2 Code Splitting Migration
React Router v7 improved code splitting. TanStack Router handles this via lazy routes:
React Router v7:
const LazyComponent = lazy(() => import('./LazyComponent'))
TanStack Router:
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/lazy-route')({
component: LazyComponent,
})
function LazyComponent() {
return <div>Lazy loaded!</div>
}
5.1 Update Link components
React Router v7:
import { Link } from 'react-router'
<Link to="/posts/123">View Post</Link>
<Link to="/posts" state={{ from: 'home' }}>Posts</Link>
TanStack Router:
import { Link } from '@tanstack/react-router'
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
<Link to="/posts" state={{ from: 'home' }}>Posts</Link>
5.2 Update navigation hooks
React Router v7:
import { useNavigate } from 'react-router'
function Component() {
const navigate = useNavigate()
const handleClick = () => {
navigate('/posts/123')
}
}
TanStack Router:
import { useNavigate } from '@tanstack/react-router'
function Component() {
const navigate = useNavigate()
const handleClick = () => {
navigate({ to: '/posts/$postId', params: { postId: '123' } })
}
}
6.1 Migrate simplified defer usage
React Router v7 simplified defer by removing the wrapper function:
React Router v7:
export async function loader() {
return {
data: fetchData(), // Promise directly returned
}
}
TanStack Router:
TanStack Router uses a different approach for deferred data. Use loading states:
export const Route = createFileRoute('/deferred')({
loader: async () => {
const data = await fetchData()
return { data }
},
pendingComponent: () => <div>Loading...</div>,
component: DeferredComponent,
})
6.2 Handle React Router v7's enhanced type safety
React Router v7 improved type inference. TanStack Router provides even better type safety:
// TanStack Router automatically infers types
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// params.postId is automatically typed as string
const post = await fetchPost(params.postId)
return { post }
},
component: Post,
})
function Post() {
// post is automatically typed based on loader return
const { post } = Route.useLoaderData()
// postId is automatically typed as string
const { postId } = Route.useParams()
}
7.1 Replace React Router v7 router creation
Before (React Router v7):
import { createBrowserRouter, RouterProvider } from 'react-router'
const router = createBrowserRouter([
// Your route definitions
])
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
)
After (TanStack Router):
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
)
8.1 React Router v7 to TanStack Router search params
React Router v7:
import { useSearchParams } from 'react-router'
function Component() {
const [searchParams, setSearchParams] = useSearchParams()
const page = searchParams.get('page') || '1'
const updatePage = (newPage) => {
setSearchParams({ page: newPage })
}
}
TanStack Router:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().catch(1),
filter: z.string().optional(),
})
export const Route = createFileRoute('/posts')({
validateSearch: searchSchema,
component: Posts,
})
function Posts() {
const navigate = useNavigate({ from: '/posts' })
const { page, filter } = Route.useSearch()
const updatePage = (newPage: number) => {
navigate({ search: (prev) => ({ ...prev, page: newPage }) })
}
}
Only after everything is working with TanStack Router:
9.1 Remove React Router v7
npm uninstall react-router
9.2 Clean up unused imports
Search your codebase for any remaining React Router imports:
# Find remaining React Router imports
grep -r "react-router" src/
Remove any remaining imports and replace with TanStack Router equivalents.
10.1 Configure strict TypeScript
Update your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
10.2 Add search parameter validation
For routes with search parameters, add validation schemas:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const postsSearchSchema = z.object({
page: z.number().min(1).catch(1),
search: z.string().optional(),
category: z.enum(['tech', 'business', 'lifestyle']).optional(),
})
export const Route = createFileRoute('/posts')({
validateSearch: postsSearchSchema,
component: Posts,
})
Before deploying your migrated application:
Problem: You have remaining React Router imports that conflict with TanStack Router.
Solution:
grep -r "react-router" src/
Problem: TypeScript showing errors about route parameters not being typed correctly.
Solution:
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
Problem: Missing SSR or code splitting functionality after migration.
Solution:
Problem: Routes not rendering or 404 errors for valid routes.
Solution:
Problem: v7's simplified defer or other features don't have direct equivalents.
Solution:
| Feature | React Router v7 | TanStack Router |
|---|---|---|
| Type Safety | Good | Excellent |
| File-based Routing | Framework mode only | Built-in |
| Search Params | Basic | Validated with schemas |
| Code Splitting | Good | Excellent with lazy routes |
| SSR | Framework mode | Built-in with TanStack Start |
| Bundle Size | Larger | Smaller |
| Learning Curve | Moderate | Moderate |
| Community | Large | Growing |
After successfully migrating to TanStack Router, consider these enhancements: