For the better part of a decade, if you were building a Single Page Application (SPA) in React, the conversation about routing started and ended with React Router. It was the de facto standard, the “nobody got fired for choosing IBM” option of the React ecosystem.
But things have changed. As we settle into 2025, the demands on frontend architecture have shifted heavily toward TypeScript-first development and end-to-end type safety. We aren’t just looking for a library that switches components based on the URL anymore; we want a library that treats the URL as a strictly typed state manager, preventing entire classes of bugs before we even hit “save.”
Enter TanStack Router (formerly generic-fs-router). Created by Tanner Linsley (the brain behind React Query/TanStack Query), it challenges the status quo by offering a router built from the ground up for type inference. Meanwhile, React Router hasn’t stood still, merging with Remix functionality to offer v7.
In this article, we’re going to strip away the marketing hype. We will build comparable implementations in both libraries, analyze the developer experience (DX), look at the performance implications, and help you decide which engine should drive your next application.
1. Prerequisites and Environmental Setup #
Before we dive into the code, let’s establish the baseline. We assume you are working with a modern React stack.
Requirements:
- Node.js: v20+ (LTS)
- React: v19 (Concurrent features enabled)
- TypeScript: v5.5+ (Strict mode is mandatory for this comparison)
- IDE: VS Code (with the official extensions for both libraries highly recommended)
For the code examples below, we assume a standard Vite scaffolding:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm installThe Dependencies #
You will need to install the specific router you are testing.
For React Router (v7 context):
npm install react-router-dom localforage match-sorter sort-byFor TanStack Router:
npm install @tanstack/react-router @tanstack/router-devtools2. The Incumbent: React Router (Modern Data APIs) #
React Router significantly evolved with the introduction of Data APIs (loaders and actions). If you are still using strictly <Route> components inside a generic <BrowserRouter> without loaders, you are using the library as if it were 2020.
Let’s look at a modern implementation where we fetch a user profile based on a URL parameter.
The Setup #
We use createBrowserRouter to enable data fetching decoupling.
// src/router-setup.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { UserPage, userLoader } from "./UserPage";
import { ErrorPage } from "./ErrorPage";
const router = createBrowserRouter([
{
path: "/",
element: <div>Home Page</div>,
errorElement: <ErrorPage />,
},
{
path: "users/:userId",
element: <UserPage />,
loader: userLoader, // The data fetching logic lives here
},
]);
export function App() {
return <RouterProvider router={router} />;
}The Component & Loader #
Here lies the friction point in 2025. While React Router now supports TypeScript, the connection between the path definition and the component hooks is often loose. You have to cast types or rely on generics that aren’t inferred automatically from the route configuration itself.
// src/UserPage.tsx
import { useLoaderData, Params } from "react-router-dom";
interface User {
id: string;
name: string;
role: string;
}
// 1. We have to manually type the params
export async function userLoader({ params }: { params: Params<"userId"> }) {
if (!params.userId) throw new Error("No User ID");
// Simulate API call
const response = await fetch(`/api/users/${params.userId}`);
return response.json() as Promise<User>;
}
export function UserPage() {
// 2. The type of 'user' is often 'unknown' or requires manual casting
// unless you use specific helper utilities.
const user = useLoaderData() as User;
return (
<div className="p-4 border rounded shadow">
<h1>{user.name}</h1>
<p>Role: {user.role}</p>
</div>
);
}The Critique: It works, and it’s stable. However, if I change the path from users/:userId to users/:id in the router config, TypeScript won’t warn me inside UserPage that I’m accessing a parameter that no longer exists until runtime (or unless I manually update my types).
3. The Challenger: TanStack Router #
TanStack Router takes a “Type Safety or Death” approach. It requires a bit more boilerplate upfront to generate the route tree, but the payoff is intellisense that knows your API better than you do.
The Setup (Code-Based Routing) #
While TanStack Router supports file-based routing (which is excellent), we will use code-based routing here to keep the comparison fair and visible in one file.
// src/tanstack-setup.tsx
import {
createRouter,
createRoute,
createRootRoute,
RouterProvider
} from '@tanstack/react-router';
import { z } from 'zod'; // Zod is great for validation
// 1. Create a Root Route
const rootRoute = createRootRoute({
component: () => <Outlet />, // Helper imports assumed
});
// 2. Define the route
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Home</div>,
});
// 3. Define a dynamic route with validation
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'users/$userId',
// AUTOMATIC TYPE SAFETY: The loader knows $userId exists
loader: async ({ params }) => {
const response = await fetch(`/api/users/${params.userId}`);
return response.json();
},
component: UserComponent,
});
// 4. Create the route tree
const routeTree = rootRoute.addChildren([indexRoute, userRoute]);
// 5. Create the router
const router = createRouter({ routeTree });
// 6. Register types for global intellisense
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
export function App() {
return <RouterProvider router={router} />;
}The Component (Type Nirvana) #
Here is where the magic happens. We don’t import generic hooks; we import hooks that read the specific route definition.
// src/UserComponent.tsx
import { useLoaderData } from '@tanstack/react-router';
// Import the specific route definition (or use generated types)
import { userRoute } from './tanstack-setup';
export function UserComponent() {
// NO CASTING REQUIRED.
// 'user' is inferred directly from the return type of the loader defined above.
const user = userRoute.useLoaderData();
return (
<div className="p-4 border rounded shadow">
<h1>{user.name}</h1>
<p>ID: {user.id}</p>
</div>
);
}If you try to link to /users/123 but type it as /user/123, your IDE will throw a red squiggly line. That is the 2025 standard.
4. Feature Comparison: The Search Params Battle #
The biggest differentiator in modern SPAs is how we handle Search Parameters (Query Strings).
In traditional routing, search params are just a string that you have to parse. In TanStack Router, search params are validated state.
Handling Search Params Comparison #
| Feature | React Router | TanStack Router |
|---|---|---|
| Storage | String (URLSearchParams) |
JSON Object (Serialized) |
| Validation | Manual (parse inside component) | Built-in (Zod/Valibot integration) |
| Type Safety | Loose (mostly `string | null`) |
| Updating | setSearchParams |
navigate({ search: (old) => new }) |
Let’s visualize the data flow difference using a Mermaid diagram.
The TanStack Search Param Code #
This is arguably the “killer feature” that makes developers switch.
// validating search params
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'users',
// Validate that 'page' is a number and 'sort' is a specific string
validateSearch: (search) => {
return {
page: Number(search?.page ?? 1),
sort: (search?.sort as 'asc' | 'desc') || 'asc',
};
},
});
function UsersList() {
// 'page' is typed as number, 'sort' is typed as 'asc' | 'desc'
const { page, sort } = usersRoute.useSearch();
return <div>Page: {page}</div>;
}If you try to navigate to this route programmatically without providing the required search params (if they weren’t optional), TypeScript would prevent the build.
5. Performance and Bundle Size #
In the world of Core Web Vitals, bytes matter.
- React Router (dom): ~20kb minzipped. It’s not heavy, but it includes a lot of legacy compatibility code.
- TanStack Router: ~12kb minzipped (varies based on tree-shaking).
However, raw size isn’t the only metric. TanStack Router implements sophisticated Smart Caching for its loaders. It treats route data similarly to how React Query treats server state. It has built-in stale-while-revalidate logic.
With React Router, if you navigate away from a page and come back, the loader typically fires again (unless you implement your own caching layer). TanStack Router keeps that data cached in memory, configurable per route, resulting in “instant” navigation feels for the user.
6. When to Use Which? #
This is not a “React Router is dead” post. React Router is incredibly stable, has massive community support (StackOverflow answers for days), and is the engine behind Remix.
Choose React Router if: #
- You are migrating a legacy app: The migration path from v5/v6 to v7 is clearer than rewriting to TanStack.
- You are using Remix: It’s the native router.
- Team familiarity: If your team struggles with advanced TypeScript generics, React Router is more forgiving (at the cost of runtime safety).
Choose TanStack Router if: #
- Greenfield Project: Starting fresh in 2025? It is hard to justify not using TanStack.
- Complex State in URL: If your app has complex filters, sorting, and pagination stored in the URL, TanStack’s search param handling is superior.
- Strict Type Safety: You want full type safety across boundaries.
- Performance: You want built-in SWR (Stale-While-Revalidate) caching for route data without installing React Query (though they work great together).
7. Conclusion #
The landscape of React navigation has matured. We are no longer just mapping components to paths; we are defining the data requirements and state schema of our application via the URL.
React Router v7 is a robust, battle-tested workhorse that gets the job done reliably. However, TanStack Router feels like the tool designed for the modern era of TypeScript engineering. It forces you to write better code by making invalid states unrepresentable.
For my professional projects this year, specifically large-scale dashboards and SaaS platforms, I am betting on TanStack Router. The initial learning curve of the “builder pattern” setup pays dividends every time I refactor a route and my IDE automatically renames every link in the application.
Further Reading #
Disclaimer: Technologies evolve rapidly. Always check the official documentation for the absolute latest API changes before pushing to production.