Skip to content

Web Development · Frontend

TanStack Router in 2026: Type-Safe Routing for React

TanStack Router brings full TypeScript inference to URL params, search params, and loader data. Here's what that looks like in practice and when it's worth adopting.

Anurag Verma

Anurag Verma

7 min read

TanStack Router in 2026: Type-Safe Routing That Rewires How You Think About Navigation

Sponsored

Share

React Router has been the default for React SPA routing for years, and it works. But “works” has a ceiling. Navigate to /users/abc123 and params.userId is a string. Pass ?page=2&sort=name as search params and you’re parsing strings by hand. Load data in a route loader and the type of that data is unknown unless you add assertions. At every boundary, you write boilerplate to get from the URL to typed data.

TanStack Router (v1 reached stable in late 2023, and it’s been in steady production use through 2025 and 2026) solves these problems with full TypeScript inference through the routing layer, as the official TanStack Router docs lay out. Params are typed. Search params have schemas with defaults. Loader data is inferred automatically. The type errors come from the router, not from your downstream code.

It’s a different way of thinking about routing, and for TypeScript-first teams, it’s hard to go back once you’ve used it.

The Core Idea: Routes as Type Boundaries

In most routers, routes are runtime constructs. You define them with strings, and TypeScript doesn’t know what params or search params each route accepts. TanStack Router defines routes as typed objects where the type information flows through the rest of the application.

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/users/$userId")({
  loader: async ({ params }) => {
    // params.userId is typed as string here — inferred from the route path
    const user = await fetchUser(params.userId);
    return { user };
  },
  component: UserDetail,
});

function UserDetail() {
  const { user } = Route.useLoaderData();
  // user is the return type of fetchUser — no type assertion needed
  return <div>{user.name}</div>;
}

The route’s param type ($userId), loader data type, and component are all linked. Change the loader’s return type and the component’s useLoaderData() call updates automatically.

File-Based Routing

TanStack Router supports both code-based and file-based routing. The file-based approach is the more ergonomic one for most applications, especially if you’ve already gone through a Vite 8 or Rolldown migration and have the plugin pipeline set up.

Set up the Vite plugin:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [TanStackRouterVite(), react()],
});

With the plugin, files in src/routes/ become routes automatically. The plugin watches the files and generates a routeTree.gen.ts file with all the type information. You never write or edit this file manually.

Route file naming:

src/routes/
  __root.tsx          → / (root layout)
  index.tsx           → /
  users.tsx           → /users (layout for /users/*)
  users/
    index.tsx         → /users
    $userId.tsx       → /users/:userId
    $userId.edit.tsx  → /users/:userId/edit
  settings/
    index.tsx         → /settings
    profile.tsx       → /settings/profile

Layouts are expressed with parent route files. users.tsx is the layout for anything under /users/. Nested routes render inside <Outlet /> in the parent.

Search Params with Schemas

URL search params are where the ergonomic difference is most obvious. Most routers give you raw strings. TanStack Router lets you declare a schema with validation and defaults.

import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

export const Route = createFileRoute("/users")({
  validateSearch: z.object({
    page: z.number().int().positive().default(1),
    sort: z.enum(["name", "email", "createdAt"]).default("name"),
    q: z.string().optional(),
  }),
  component: UserList,
});

function UserList() {
  const { page, sort, q } = Route.useSearch();
  // page is number, sort is "name" | "email" | "createdAt", q is string | undefined
  // Accessing a key that doesn't exist in the schema is a type error
  
  return (
    <div>
      <SearchBar defaultValue={q} />
      <UserTable sortBy={sort} />
      <Pagination current={page} />
    </div>
  );
}

Updating search params is also typed:

import { useNavigate } from "@tanstack/react-router";

function Pagination({ current }: { current: number }) {
  const navigate = useNavigate({ from: "/users" });
  
  return (
    <button
      onClick={() =>
        navigate({
          search: (prev) => ({ ...prev, page: current + 1 }),
          // TypeScript enforces the shape — you can't pass page: "next"
        })
      }
    >
      Next page
    </button>
  );
}

The search updater function receives the current typed search state and returns a new one. Invalid shapes are type errors at build time.

Loaders and the Data Loading Model

TanStack Router integrates with a data loading model that’s closer to Remix than React Router v6’s optional loaders.

Route loaders run before the component renders and block navigation until they resolve. This means the component always receives fully loaded data, no loading states in the component itself.

// src/routes/projects/$projectId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { queryClient } from "../../query-client";
import { projectQueryOptions } from "../../queries/projects";

export const Route = createFileRoute("/projects/$projectId")({
  loader: ({ params }) =>
    queryClient.ensureQueryData(projectQueryOptions(params.projectId)),
  component: ProjectDetail,
});

function ProjectDetail() {
  const project = Route.useLoaderData();
  // project is the resolved type from projectQueryOptions — not undefined, not loading
  
  return <h1>{project.name}</h1>;
}

This pairs well with TanStack Query. Use queryClient.ensureQueryData() in the loader to pre-populate the query cache, then useQuery() in the component for subsequent refetches. The first render is always from the pre-loaded cache.

Navigation links are also type-checked. Passing a URL string to <Link> is a type error if the route doesn’t exist:

import { Link } from "@tanstack/react-router";

// Correct — /users/$userId exists
<Link to="/users/$userId" params={{ userId: user.id }}>
  {user.name}
</Link>

// Type error — /user/$id is not a registered route
<Link to="/user/$id" params={{ id: user.id }}>
  {user.name}
</Link>

// Type error — userId param is required but missing
<Link to="/users/$userId">
  {user.name}
</Link>

This catches broken links at compile time. In a codebase where routes change occasionally, this is more valuable than it sounds.

When to Use TanStack Router vs React Router

TanStack Router is a better fit when:

  • TypeScript is used consistently throughout the project
  • URL state (search params) carries significant application state
  • Data loading is coupled to routes and benefits from typed loader data
  • You want type-safe links and navigation

React Router is a better fit when:

  • The project is JavaScript-only or TypeScript is loosely applied
  • The routing is simple: few params, minimal search state, no loaders
  • The team is already familiar with React Router and the complexity is manageable, including through a React Router v8 upgrade
  • You need features TanStack Router doesn’t cover yet (e.g., some edge cases in server-side rendering)

Next.js App Router is a separate consideration. For apps built on Next.js, the App Router handles routing, and TanStack Router is redundant. TanStack Router is for SPAs and Vite-based apps primarily, though TanStack Start (a full-stack meta-framework built on TanStack Router) is worth watching for teams who want the router in a server-rendered setup.

Migration from React Router

A full migration can be staged. The TanStack Router docs suggest starting with a wrapper approach:

  1. Install TanStack Router alongside React Router
  2. Convert routes one by one to TanStack Router, wrapping the existing React Router component tree temporarily
  3. Remove React Router once all routes are converted

For smaller apps (under 20 routes), a full migration in a day is realistic. For larger apps, the staged approach avoids a big-bang rewrite.

The most time-consuming part is usually extracting loader data from components (where it was fetched with useEffect) into route loaders. This is also where you see the biggest gains: components shrink substantially when data fetching moves into the router.


TanStack Router won’t make sense for every React project. The setup cost is real, and for simple apps with a handful of routes and no complex URL state, React Router gets you there without the overhead. But for apps where TypeScript precision matters and where URL state drives significant behavior, the typed routing layer changes the experience of working in the codebase. You stop writing defensive code for state that might not exist, because the router guarantees it does.

Frequently asked questions

Do I need to rewrite my whole app to adopt TanStack Router?
No. The TanStack Router docs describe a staged migration: install it alongside React Router, convert routes one by one while wrapping the existing React Router tree temporarily, then remove React Router once everything is converted. For apps under 20 routes, a full migration in a day is realistic; larger apps benefit from the staged approach.
How is this different from just using TypeScript with React Router?
React Router doesn't know what params or search params a given route accepts, so you're still writing assertions or manual parsing at each boundary. TanStack Router defines routes as typed objects, so the param type, the search param schema, and the loader's return type all flow through automatically, including into `useLoaderData()` and `<Link>`.
What happens if I use file-based routing?
Files in src/routes/ become routes automatically once you add the Vite plugin. It generates a routeTree.gen.ts file containing all the type information, which you never edit manually. Parent route files, like users.tsx, act as layouts for everything nested underneath, rendered through an `<Outlet />`.
Should I use TanStack Router or Next.js App Router?
They're not really competitors. TanStack Router is for SPAs and Vite-based apps; if you're already on Next.js, the App Router handles routing and TanStack Router is redundant. TanStack Start, a full-stack meta-framework built on TanStack Router, is the closer comparison for teams that want this router in a server-rendered setup.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored