Two mature React routers, two opposite defaults.
Both can ship the same SPA, dashboard, or full-stack app. The decision comes down to what you want routing to be in your architecture: a UI-mapping layer you can progressively enhance (React Router), or a typed URL state system where params, search, and loader data are validated and inferred end-to-end by default (TanStack Router).
That answer comes down to a few architectural constraints about how stateful the URL is, whether you need server rendering on day one, and where the team already has investment.
Link to headingTwo routing philosophies
React Router's default is UI-first. A URL maps to a component, and data fetching, search-param parsing, and types get layered on top.
TanStack Router's default is state-first. A URL maps to validated state, then to loader data, then to UI, with type inference for path params, search params, and loader data falling out of the route definition automatically.
Neither default is correct in the abstract. React Router's model fits teams who want to own how data flows and gradually layer capabilities onto routing. TanStack Router's model is newer, and it fits teams that treat the URL as a first-class data structure and want the compiler to catch invalid state early. Which one fits depends on the application's shape and what the team already has in motion.
React Router has been on npm since 2014. TanStack Router hit stable v1.0.0 in December 2023. Both are in active development, and both have production adoption at scale.
Link to headingPick the React Router mode first
React Router v7 ships in three modes, and the comparison with TanStack Router changes significantly depending on which mode you are evaluating. The official modes documentation puts it clearly: each mode builds on the last. Moving from declarative to data to framework adds capabilities, but it also narrows your architectural choices.
Declarative mode gives you
<Link>,useNavigate,useLocation, and URL-to-component matching, nothing more. Routing is the surface; data fetching lives in components.Data mode, configured through
createBrowserRouter, moves route configuration outside React rendering and addsloader,action,useFetcher, and pending states without adopting framework mode's Vite plugin, file conventions, or built-in rendering strategy.Framework mode wraps data mode with a Vite plugin and turns React Router into the Remix architecture. It adds type-safe
href, a codegen-based Route Module API, intelligent code splitting, and a choice of SPA, SSR, or static rendering strategies.
TanStack Router is a different comparison depending on which React Router mode you mean:
A TanStack Router SPA is closest in scope to React Router's data mode, not framework mode.
TanStack Start, the full-stack framework built on TanStack Router, is the closest comparison to the React Router framework mode.
Declarative mode and TanStack Router are not really in the same category. One is a thin routing layer; the other is an opinionated typed state system.
Each comparison below names the React Router mode it describes, because the right comparison depends on which mode you mean.
Link to headingType safety with a worked example
This is the axis where the two routers make the sharpest architectural tradeoff.
In TanStack Router’s file-based setup, type safety starts with a generated route tree. The Vite plugin or CLI generates a routeTree.gen.ts file from your filesystem, and TanStack Router then uses TypeScript inference over that route tree to carry types through the routing APIs. Path params are inferred from route paths, search params from validateSearch, and loader data from each route’s loader, flowing into APIs like Route.useParams(), Route.useSearch(), and Route.useLoaderData() without manually writing those hook return types.
TanStack Router provides convenient APIs for validating and typing search params, and this all starts with the route's validateSearch option.
React Router v7 framework mode generates types per route into a .react-router/types/ directory, exposed as Route.LoaderArgs, Route.ActionArgs, and Route.ComponentProps.
The React Router team documented why codegen is necessary: TypeScript cannot use the filesystem for type inference or type checking, and the only tenable way to infer types from file paths is through code generation.
They also acknowledge the standard objection. Typegen solutions often receive criticism due to typegen'd files becoming out of sync during development. The mitigation is a --watch flag and Vite plugin integration.
Both approaches can be type-safe, but they pay for it in different ways:
TanStack Router puts the cost in TypeScript itself. Deep inference can increase compile times, especially as the route tree and schemas grow.
React Router v7 (framework mode) puts the cost in tooling. You get strong types via code generation, but you need the typegen step (and watch mode) to stay in sync during development.
React Router outside framework mode is more DIY.
URLSearchParamsis stringly typed, and route params stay untyped until you add your own parsing, casting, and validation.
Here is the same dashboard route with filter, pagination, and sort params in each router.
TanStack Router, file-based route with Zod-validated search params:
import { z } from 'zod' import { createFileRoute } from '@tanstack/react-router'const dashboardSearchSchema = z.object({ filter: z.string().catch(''), page: z.number().catch(1), sort: z.enum(['newest', 'oldest', 'priority']).catch('newest'), })export const Route = createFileRoute('/dashboard')({ validateSearch: (search) => dashboardSearchSchema.parse(search), loaderDeps: ({ search: { filter, page, sort } }) => ({ filter, page, sort }), loader: ({ deps: { filter, page, sort } }) => fetchIssues({ filter, page, sort }), component: DashboardRoute, })function DashboardRoute() { const { filter, page, sort } = Route.useSearch() const issues = Route.useLoaderData() return <IssueList filter={filter} page={page} sort={sort} issues={issues} /> }React Router v7 framework mode, codegen-typed loader and action:
import type { Route } from './+types/dashboard' import { Form } from 'react-router' import { fetchIssues, updateIssue } from '../lib/issues'export async function loader({ request }: Route.LoaderArgs) { const url = new URL(request.url) const filter = url.searchParams.get('filter') ?? '' const page = Number(url.searchParams.get('page') ?? '1') const sort = url.searchParams.get('sort') ?? 'newest' const issues = await fetchIssues({ filter, page, sort }) return { filter, page, sort, issues } }export async function action({ request }: Route.ActionArgs) { const formData = await request.formData() const id = String(formData.get('id')) return updateIssue(id, { resolved: true }) }export default function Dashboard({ loaderData }: Route.ComponentProps) { const { filter, page, sort, issues } = loaderData return ( <Form method="post"> <IssueList filter={filter} page={page} sort={sort} issues={issues} /> </Form> ) }The TanStack Router version declares the schema once. The type of filter, page, and sort flows through loaderDeps, into the loader, and back out through Route.useSearch() without any additional declarations.
The React Router version gets full type safety on loaderData and action arguments through the generated Route types, but search-param parsing is manual string handling inside the loader body. Adding validation or typing to those params means running a schema validator such as Zod inside the loader yourself, rather than declaring it at the route boundary.
Link to headingRoute definition at scale
How the route tree is defined matters more at 200 routes than at 10.
React Router has three route-definition shapes:
Declarative mode uses nested
<Route>JSX. It is the most visual style and the most familiar to engineers who have used React Router for years, but it does not scale to typed search params on every route.Data mode moves the route tree out of JSX into a route-objects array passed to
createBrowserRouter.Framework mode reads a file convention (
app/routes/) plus anapp/routes.tsconfiguration file.
TanStack Router defaults to file-based routes generated by the @tanstack/router-plugin, with code-based routes available for teams that prefer explicit configuration. A route file at src/routes/dashboard.tsx becomes the /dashboard route; the plugin generates a route tree that feeds into the type system directly.
At scale, what matters is having a route tree that serves as a single source of truth and is easy to search and refactor. Both file-based approaches deliver that.
The distinction is where types come from. TanStack Router's typed Link and Route.useNavigate reflect the full route tree back into the type system. With code-based routing this needs no separate codegen step; with file-based routing (the default), the Vite plugin generates a routeTree.gen.ts that feeds the same inference. React Router framework mode's codegen gives you the same guarantee for loader and action args but requires react-router typegen to stay current.
Link to headingData loading and mutations
React Router's model is route-owned data plus built-in mutation plumbing.
In data mode and framework mode, every route owns a loader for data, and action handles mutations. Submitting a <Form> triggers the action, and when the action completes, all loader data on the page revalidates automatically, keeping the UI in sync without manual cache invalidation.
TanStack Router's model is route-owned data plus composable pre-load context.
Its loaders are per-route and pair with a beforeLoad hook that builds shared context, such as auth state and feature flags, before any sibling loader runs. The lifecycle runs sequentially up the parent chain. A throw in a parent beforeLoad halts every child's load, which makes authorization checks composable but also means the semantics are worth understanding before you rely on them for complex trees.
The caching difference is more direct:
TanStack Router ships built-in SWR caching, a long-term in-memory caching layer for route loaders that handles deduplication, preloading, stale-while-revalidate, and background refetching on a per-route basis.
React Router does not ship a first-class loader cache; loader data is fresh on every navigation unless you wire in an external cache. This is by design for teams that prefer fresh-on-navigation semantics or already run an external cache.
The common pairing for teams that want richer query patterns and shared cache state is TanStack Query, which integrates with TanStack Router's loader lifecycle.
TanStack Router does not ship a first-class <Form> and action equivalent. Mutations run through whatever data layer the rest of the application uses, whether that is a query client, a server function, or an RPC. This is not a gap if the mutation pattern is already decided, but it is worth planning up front before the route tree grows.
Link to headingWhat each router library is best at
TanStack Router is well-suited to client-heavy SPAs, dashboards, admin panels, internal tools, and search-driven UIs where the URL carries rich application state. Multiple filters, sorting, pagination, view modes, and combinations of these belong in the URL, and they need validation and typed access. Typed search-param schemas and per-route caching are load-bearing in those apps. If an invalid filter string in the URL should be caught at the type boundary rather than inside a component at runtime, TanStack Router has the clearer default.
React Router framework mode fits full-stack apps that want loaders, actions, server rendering, and the Remix-shaped data flow on day one. Teams already invested in Remix v2 or React Router v6 with the v7 future flags on have a well-defined path into framework mode. The architecture is familiar, and the accumulated knowledge transfers.
React Router data mode (
createBrowserRouter) fits SPAs that want loaders, actions, and<Form>mutations without committing to framework mode's SSR story or codegen step. It is a solid middle path for teams that outgrew declarative mode but are not building a server-rendered app.React Router declarative mode is well-suited to small SPAs and prototypes where the route tree is short and data fetching lives in components rather than in loaders.
Many applications could go either way. If your app’s shape does not make the decision obvious, your team’s existing investment should carry real weight. Choose the router that fits the project, rather than forcing the project into a new mental model for no workload-specific reason.
Link to headingMigration in both directions
React Router v6 to v7 is a gradual migration behind future flags. Teams that turned the flags on incrementally can typically finish in days. The path is well-documented, and the API surface is deliberately stable across the transition.
Remix v2 to React Router v7 framework mode is largely a rename and an import change in most files. The Remix team has explicitly framed v7 framework mode as the continuation of Remix after the two projects merged.
React Router, in any mode, is a rewrite of the route layer. Route definitions, search-param parsing, and any framework-mode loaders and actions all need to be ported. Apps that have leaned into React Router's
<Form>andactionAPIs lose the most in this migration, because TanStack Router has no first-class equivalent for that pattern.TanStack Router to React Router is less common and similarly a full rewrite of route definitions. In practice, teams consider moving from TanStack Router to React Router in two situations: when TypeScript compile times become a real constraint at scale, or when they need React Router’s more mature
<Form>andactionworkflow for data mutations.
In both directions, the realistic unit of work is the route tree plus loaders plus search-param handling. Components and query logic usually port unchanged.
Link to headingDeployment on Vercel is the same
Whichever router you pick, deployment on Vercel follows the same path.
To deploy React Router v7 framework mode to Vercel's Fluid compute, you should install the
@vercel/react-routerpackage to enable the full feature set and add thevercelPreset()to yourreact-router.config.ts.To deploy TanStack Start (the full-stack framework built on TanStack Router) to Fluid compute, you must install the
nitropackage and add thenitro()plugin to yourvite.config.ts.
The TanStack Start on Vercel docs and the React Router on Vercel docs cover this setup in full.
The router decision is an application-architecture choice. Scaling behavior, the deployment surface, and the pricing model (Active CPU billing) are identical regardless of which way the team goes, provided the correct Vite adapters are configured.
If teams later build agents on top of either router, the same Vercel primitives are available: the deployment surface through CLI, API, MCP, and git; AI Gateway for model access; Vercel Sandbox for safe execution of generated code; and Vercel Workflows for durable execution for long-running, multi-step work.
Link to headingA short decision checklist
Run these questions against the specific project at hand to determine the right routing library.
Does the URL carry rich application state, with multiple filters, pagination, sort, or view modes, that needs validation and typed access throughout the component tree?
TanStack Router is shaped for that workload.
Does the app need server rendering, server actions, or the Remix data-flow architecture on day one?
React Router v7's framework mode is designed around that starting point.
Is the team already on Remix v2 or React Router v6 with v7 future flags on?
React Router keeps that investment in place, in whatever mode matches the app's shape.
Is the app a small SPA where data fetching lives in components and the route tree stays short?
React Router declarative mode covers the use case without adding surface area you will not use.
Is end-to-end type inference without a codegen step a hard requirement?
TanStack Router’s code-based routing is designed around that model, while its file-based routing can still use a generated route tree configuration.
Does the project have existing TanStack Query investment that the router should integrate with?
TanStack Router pairs naturally with TanStack Query through its loader lifecycle.
Is a first-class
<Form>and action surface needed without writing the mutation layer?React Router data mode and framework mode both ship this out of the box; TanStack Router does not.
Both routers are mature, production-ready libraries with significant adoption and active maintenance. Both deploy to the same infrastructure in the same way.
Which router fits your project depends on your app’s shape: how much state lives in the URL, whether you need server rendering, and what your team already knows well. If the checklist does not point clearly in one direction, team familiarity is a perfectly reasonable deciding factor.
Link to headingFrequently asked questions
Is TanStack Router production ready?
Yes. Stable v1.0.0 shipped in December 2023, and in May 2026, the library moved roughly 17 million weekly npm downloads.
Teams are running it in production across SPAs, dashboards, and internal tools. TanStack Start, the full-stack framework built on TanStack Router, has reached a stable v1 release
Does React Router v7 replace Remix?
Functionally, yes. React Router v7 framework mode is what Remix v2 became after the two projects merged.
The loader and action architecture, the Vite plugin, the file-based route convention, and the repo itself (remix-run/react-router) are all continuous. Teams running Remix v2 can treat v7 framework mode as a rename with a small import change in most files.
Can I run TanStack Router and React Router in the same app?
Not in the same render tree. They are alternative top-level routers; only one can own the URL at a time.
You can stage a migration route-by-route by extracting subtrees into a new shell application, but inside a single render tree, you pick one.
Do I need TanStack Query if I use TanStack Router?
No. TanStack Router ships built-in SWR caching for loader data that handles deduplication, preloading, and stale-while-revalidate on a per-route basis.
Teams that want richer mutation patterns, shared cache state across components, or more control over invalidation often add TanStack Query on top, but it is an optional pairing rather than a required dependency.