Loading...
Loading...
Step-by-step migration from React Router v7 to TanStack Router: route definition conversion, Link/useNavigate API differences, useSearchParams to validateSearch + useSearch, useParams with from, Outlet replacement, loader conversion, code splitting differences.
npx skill4agent add tanstack/router migrate-from-react-routerCRITICAL: If your UI is blank after migration, open the console. Errors like "cannot use useNavigate outside of context" mean React Router imports remain alongside TanStack Router imports. Uninstall(andreact-routerif present) to surface them as TypeScript errors.react-router-domCRITICAL: TanStack Router uses+tofor navigation, NOT template literal paths. Never interpolate params into theparamsstring.toNOTE: React Router v7 recommends importing from(notreact-router). Thereact-router-dompackage still exists but just re-exports fromreact-router-dom. Check for imports from both.react-router
git checkout -b migrate-to-tanstack-routernpm install @tanstack/react-router @tanstack/react-router-devtools
npm install -D @tanstack/router-plugin// 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({ target: 'react', autoCodeSplitting: true }),
react(),
],
})mkdir src/routes<BrowserRouter>createBrowserRouter([{ element: <Layout />, children: [...] }])src/routes/__root.tsx// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Outlet />
<TanStackRouterDevtools />
</>
),
})// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
ReactDOM.createRoot(rootElement).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}createFileRoute// Defined in route config array
{ path: '/posts', element: <Posts />, loader: postsLoader }// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts()
return { posts }
},
component: PostsPage,
})
function PostsPage() {
const { posts } = Route.useLoaderData()
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}/posts/:postId/posts/$postId// 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: PostPage,
})
function PostPage() {
const { post } = Route.useLoaderData()
return <article>{post.title}</article>
}<Link>import { Link } from 'react-router'
;<Link to={`/posts/${postId}`}>View Post</Link>import { Link } from '@tanstack/react-router'
;<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>toparamsactiveProps={{ className: 'font-bold' }}data-status="active"useNavigateimport { useNavigate } from 'react-router'
const navigate = useNavigate()
navigate(`/posts/${postId}`)import { useNavigate } from '@tanstack/react-router'
const navigate = useNavigate()
navigate({ to: '/posts/$postId', params: { postId } })useSearchParamsvalidateSearchuseSearchimport { useSearchParams } from 'react-router'
function Posts() {
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page')) || 1
const goToPage = (p: number) => setSearchParams({ page: String(p) })
}// In the route definition:
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
export const Route = createFileRoute('/posts')({
validateSearch: z.object({
page: z.number().default(1).catch(1),
}),
component: Posts,
})
// In the component:
import { useNavigate } from '@tanstack/react-router'
function Posts() {
const { page } = Route.useSearch()
const navigate = useNavigate({ from: '/posts' })
const goToPage = (p: number) => {
navigate({ search: (prev) => ({ ...prev, page: p }) })
}
}useSearch()URLSearchParamsuseParamsfromimport { useParams } from 'react-router'
const { postId } = useParams()import { useParams } from '@tanstack/react-router'
const { postId } = useParams({ from: '/posts/$postId' })const { postId } = Route.useParams()useLocationuseLocationuseLocationuseLocation()useLocation// React Router
import { useLocation } from 'react-router'
const location = useLocation()
// ❌ DON'T just swap to TanStack Router's useLocation — it's the "live" URL
import { useLocation } from '@tanstack/react-router'
// ✅ DO use the specific hook for what you need:
import {
useMatch,
useMatches,
useParams,
useSearch,
} from '@tanstack/react-router'
// Current route match (replaces most useLocation().pathname usage)
const match = useMatch({ from: '/posts/$postId' })
// All active matches (replaces useLocation for breadcrumbs/analytics)
const matches = useMatches()
// Path params (replaces useLocation + manual parsing)
const { postId } = useParams({ from: '/posts/$postId' })
// Search params (replaces useLocation().search parsing)
const { page } = useSearch({ from: '/posts' })OutletOutlet// Before
import { Outlet } from 'react-router'
// After
import { Outlet } from '@tanstack/react-router'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>
}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()
return <div>{post.title}</div>
}useLoaderData()Route.useLoaderData()useLoaderData({ from })json()autoCodeSplitting: true.lazy.tsx// src/routes/lazy-page.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/lazy-page')({
component: () => <div>Lazy loaded</div>,
})npm uninstall react-router react-router-dom
grep -r "from 'react-router" src/ # find stale imports
npx tsc --noEmit # verify clean buildLinkuseNavigateOutlet// WRONG — mixed imports
import { Link } from '@tanstack/react-router'
import { useNavigate } from 'react-router' // <- still React Router!
// CORRECT — all from TanStack Router
import { Link, useNavigate } from '@tanstack/react-router'react-routerreact-router-domuseSearchParams// WRONG — React Router pattern, returns URLSearchParams
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page'))
// CORRECT — TanStack Router pattern, returns typed object
// Route definition:
validateSearch:
z.object({
page: z.number().default(1).catch(1),
}),
// Component:
const { page } = Route.useSearch()
// page is already typed as number — no casting neededto// WRONG — React Router habit
<Link to={`/posts/${postId}`}>Post</Link>
// CORRECT — TanStack Router: path pattern + params prop
<Link to="/posts/$postId" params={{ postId }}>Post</Link>:param$paramReact Router: /posts/:postId
TanStack Router: /posts/$postId$src/routes/posts/$postId.tsx| React Router v7 | TanStack Router |
|---|---|
| |
| File-based: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Use mutations / form libraries |
| |
| |
| |