Skip to main content

TanStack Router vs React Router v7

·PkgPulse Team
0

TL;DR

TanStack Router wins on type safety — search params, path params, and loader data are all fully typed with zero configuration. React Router v7 (which merged with Remix) wins on ecosystem size, Server Components support, and the broader Next.js/Remix world. For a standalone React SPA, TanStack Router's type safety is genuinely superior. For a full-stack app with server rendering, React Router v7's loader pattern and Next.js's file-based routing are better integrated.

Key Takeaways

  • TanStack Router: 1.2M weekly downloads (growing fast), type-safe search params/loaders, devtools, ~40KB
  • React Router v7: 12M+ weekly downloads, merged with Remix, now supports RSC/server mode
  • Type safety: TanStack Router is superior — no casting, no params as { id: string }
  • File-based routing: both support it — TanStack via @tanstack/router-plugin, React Router via conventions
  • Data loading: TanStack's loaderData is typed; React Router v7's loader requires manual typing
  • For SPAs: TanStack Router is the modern choice
  • For SSR / full-stack: React Router v7 + Remix or Next.js

Download Comparison

PackageWeekly DownloadsTrend
react-router~12MStable (Remix merger)
@tanstack/react-router~1.2M+120% YoY
@remix-run/react~2.8MMerging into react-router

TanStack Router is growing extremely fast from a smaller base.


Type Safety Comparison

TanStack Router's killer feature is 100% type-safe routing. Route params, search params, and loader data are all fully typed without casting. This sounds like a minor DX improvement until you've spent time debugging a runtime error caused by a typo in a URL parameter name.

Here's the contrast in practice:

// TanStack Router — params are typed from the route definition
function PostPage() {
  const { postId } = Route.useParams();
  // postId is `string` — typed, no casting needed
  // TypeScript error if you access a param that doesn't exist in the route
}

// React Router v7 — params require explicit casting
import { useParams } from 'react-router';

function PostPage() {
  const { postId } = useParams<{ postId: string }>();
  // You must manually declare the type — the framework doesn't infer it
  // postId is `string | undefined` — even when the route always provides it
}

TanStack Router generates a route tree at build time, which TypeScript reads to provide end-to-end inference. When you rename a route parameter in the file name, TypeScript immediately flags every usage site. React Router's useParams() returns Readonly<Params<Key>> — a generic that cannot be automatically narrowed to your specific route.


Search Parameters

This is where the gap between the two libraries is most pronounced. TanStack Router makes search params first-class citizens with full schema validation:

// TanStack Router — type-safe search params with Zod
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';

const searchSchema = z.object({
  page: z.number().catch(1),
  category: z.enum(['all', 'electronics', 'clothing']).catch('all'),
  q: z.string().optional(),
  sort: z.enum(['price', 'name', 'date']).optional(),
});

export const Route = createFileRoute('/products/')({
  validateSearch: searchSchema,
  component: ProductsPage,
});

function ProductsPage() {
  // Every search param is typed — no URL parsing, no manual casting
  const { page, category, q, sort } = Route.useSearch();
  //      ^^^^   ^^^^^^^^             ^    ^^^^
  //      number  enum literal       string|undefined  enum|undefined

  const navigate = useNavigate({ from: Route.fullPath });

  return (
    <button
      onClick={() =>
        navigate({ search: (prev) => ({ ...prev, page: prev.page + 1 }) })
      }
    >
      Next page
    </button>
  );
}

With React Router's useSearchParams, you get a URLSearchParams object — untyped, requiring manual parsing and casting:

// React Router — manual search param parsing
import { useSearchParams } from 'react-router';

function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Manual parsing — no type safety
  const page = parseInt(searchParams.get('page') ?? '1', 10);
  const category = (searchParams.get('category') ?? 'all') as
    | 'all'
    | 'electronics'
    | 'clothing';
  const q = searchParams.get('q') ?? undefined;

  // Updating requires manual string construction
  setSearchParams((prev) => {
    prev.set('page', String(page + 1));
    return prev;
  });
}

For applications with complex filter UIs, paginated tables, or multi-step search flows, TanStack Router's typed search params eliminate an entire category of runtime bugs.


Data Loading

Both routers support route-level data loaders that run before the component renders. TanStack Router's advantage is that loader data is automatically typed through to the component.

// TanStack Router — loader data is fully typed
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    // params.postId: string — typed from route definition
    return fetchPost(params.postId); // Return type: Promise<Post>
  },
  component: PostPage,
});

function PostPage() {
  const post = Route.useLoaderData();
  // post: Post — inferred from loader return type, no casting
  return <h1>{post.title}</h1>;
}

React Router v7 supports loaders with typed data, but the ergonomics depend on whether you're using framework mode (with the Remix compiler that generates +types files) or SPA mode:

// React Router v7 — Framework mode with codegen (good DX)
import type { Route } from './+types/posts.$postId';

export async function loader({ params }: Route.LoaderArgs) {
  return fetchPost(params.postId);
}

export default function PostPage({ loaderData }: Route.ComponentProps) {
  // loaderData is typed via generated +types file
  return <h1>{loaderData.title}</h1>;
}

// React Router v7 — SPA mode (requires manual casting)
function PostPage() {
  const data = useLoaderData() as Post; // Manual cast required
  return <h1>{data.title}</h1>;
}

In framework mode, React Router v7 achieves similar type safety to TanStack Router through a codegen step. In SPA mode, manual casting is required.


React Router v7 and the Remix Merger

React Router v7 is not just an incremental update — it represents the merging of React Router and Remix into a single package. This is a significant architectural change that brings full-stack server-side rendering, route-level server functions, and Vite integration into React Router.

For teams building full-stack applications, React Router v7 in framework mode is now a complete solution comparable to Next.js:

// React Router v7 Framework Mode — full-stack capabilities
// app/routes/products.$productId.tsx

import type { Route } from './+types/products.$productId';
import { db } from '~/lib/db';

export async function loader({ params }: Route.LoaderArgs) {
  const product = await db.product.findUnique({
    where: { id: params.productId },
  });
  if (!product) throw new Response('Not Found', { status: 404 });
  return { product };
}

export async function action({ request, params }: Route.ActionArgs) {
  const formData = await request.formData();
  const name = formData.get('name') as string;
  await db.product.update({
    where: { id: params.productId },
    data: { name },
  });
  return redirect('/products');
}

export default function ProductPage({ loaderData }: Route.ComponentProps) {
  const { product } = loaderData;
  return (
    <div>
      <h1>{product.name}</h1>
      <form method="post">
        <input name="name" defaultValue={product.name} />
        <button type="submit">Update</button>
      </form>
    </div>
  );
}

TanStack Router v1 focuses on client-side routing with server rendering support, but it is not a full-stack framework in the same sense. For applications that need server actions, SSR, and streaming, React Router v7 or Next.js is more appropriate.


File-Based Routing Setup

Both routers support file-based routing, but TanStack Router's implementation makes type inference work automatically from file names. Setting it up with the Vite plugin is straightforward:

// vite.config.ts — TanStack Router plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';

export default defineConfig({
  plugins: [
    TanStackRouterVite(), // Generates routeTree.gen.ts automatically
    react(),
  ],
});
// routes/__root.tsx — root route wraps all pages
import { createRootRoute, Outlet, Link } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/" activeProps={{ className: 'active' }}>Home</Link>
        <Link to="/products" activeProps={{ className: 'active' }}>Products</Link>
      </nav>
      <Outlet />
      {import.meta.env.DEV && <TanStackRouterDevtools />}
    </>
  ),
});
// routes/posts/$postId.tsx — dynamic route with typed params
import { createFileRoute } from '@tanstack/react-router';
import { fetchPost } from '../api/posts';

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    // params.postId: string — inferred from filename ($postId)
    return fetchPost(params.postId);
  },
  component: PostPage,
});

function PostPage() {
  const post = Route.useLoaderData(); // Post — no casting
  const { postId } = Route.useParams(); // string — no casting
  return (
    <article>
      <h1>{post.title}</h1>
      <p>Post ID: {postId}</p>
    </article>
  );
}

The route tree is generated to routeTree.gen.ts at build time, and TypeScript reads this file to provide accurate autocomplete for every Link to= prop, every navigate() call, and every useParams() result.

One underappreciated benefit of TanStack Router's type system is that even Link components are fully typed:

// TanStack Router — Link components are type-safe
import { Link } from '@tanstack/react-router';

// TypeScript error: '/products' route has required search param 'category'
<Link to="/products" search={{ category: 'electronics', page: 1 }}>
  Electronics
</Link>

// TypeScript error: wrong type for search param
<Link to="/products" search={{ category: 123 }}>  {/* Error: number not assignable to enum */}
  Products
</Link>

// React Router — Links are not type-safe
<Link to="/products?category=electronics">Products</Link>
// No TypeScript help — strings all the way down

This extends to the navigate() function, redirect() in loaders, and useNavigate() hooks — every navigation point in the application is checked by the TypeScript compiler. In a large SPA with many routes, this eliminates an entire class of routing bugs that would otherwise only appear at runtime.

Side-by-Side

TanStack RouterReact Router v7
Downloads1.2M/week12M+/week
Type safetyEnd-to-end automaticCodegen (framework) or manual (SPA)
Search params typingZod schema, fully typedManual parsing required
SSR / Server ComponentsPartialFull (framework mode)
File-based routingVia pluginBuilt-in convention
DevtoolsBuilt-inBuilt-in
Bundle size~40KB~32KB
Full-stack framework modeNoYes (Remix merger)
EcosystemGrowingVast

Devtools and Developer Experience

Both routers ship excellent devtools for inspecting the routing state during development. TanStack Router Devtools render directly in your app (not as a browser extension) and show the full route tree, current match, search params, loader state, and pending navigations. This tight integration makes debugging complex routing scenarios significantly faster.

React Router v7 ships devtools via the @react-router/devtools package when using framework mode. The devtools surface server loader activity, action submissions, and revalidation state — features that are relevant specifically because React Router v7 supports server-side data loading.

For pure client-side SPAs, TanStack Router's devtools give better visibility into search param state and route matching details. For full-stack framework mode apps, React Router's devtools provide insights that TanStack Router doesn't need to — because TanStack Router doesn't have server loaders in the same sense.

Migration Considerations

Teams migrating from React Router v5 or v6 to v7 have a clear upgrade path — the Remix team published migration guides and the API surface is largely additive. Existing <Routes>, <Route>, useNavigate, and useParams APIs still work in library mode (non-framework mode), making incremental adoption straightforward.

Teams migrating from React Router to TanStack Router face a larger conceptual shift. The Route.useParams() pattern replaces useParams(), and the file-based route definition replaces JSX route trees. The migration is not line-for-line, but for applications where type safety is a high priority, teams consistently report the migration effort is worth it. The key practical advice: migrate one route at a time, starting with the routes that have the most complex search param or loader logic — those are the routes where TanStack Router's type system provides the most immediate value.

Package Health

PackageWeekly DownloadsMaintainers
react-router~12MShopify team (Remix) + community
@tanstack/react-router~1.2MTanner Linsley + TanStack team
@tanstack/router-devtools~900KTanStack team

Both packages are actively maintained. React Router v7's download count includes all React Router users — the installed base is enormous, which means stability guarantees and ecosystem support are very strong. TanStack Router is growing rapidly and the team has a strong track record with TanStack Query, Table, and Form.


When to Choose

Choose TanStack Router when:

  • Building a React SPA where client-side routing is the primary concern
  • TypeScript is a hard requirement and type-safe search params matter significantly
  • You have complex query parameter state (filters, pagination, search) that needs schema validation
  • You want automatic loader typing without a codegen step or framework mode
  • Your application does not require server-side rendering or Server Components

Choose React Router v7 when:

  • Building a full-stack application that needs SSR, streaming, or server actions
  • You're migrating from Remix and want a direct upgrade path
  • You need the largest possible ecosystem compatibility and community resources
  • Your team already has React Router expertise and does not need to retrain
  • You want framework mode with Vite integration and a Next.js-like developer experience

The routing layer is one of the most foundational decisions in a React application. TanStack Router's bet is that TypeScript-first type safety across every navigation surface is worth adopting a newer library. React Router v7's bet is that a battle-tested library with the full power of the Remix architecture gives teams everything they need for both client and server routing. Both bets are reasonable — the right choice depends on whether your application needs the server-rendering story that React Router v7 uniquely delivers.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.