DevToolBoxฟรี
บล็อก

TanStack Router Complete Guide: Type-Safe Routing for React (2026)

18 min readโดย DevToolBox Team

TanStack Router is a fully type-safe routing library for React that brings end-to-end type safety to route params, search params, loaders, and navigation. Unlike traditional routers that rely on string-based paths and runtime validation, TanStack Router catches routing errors at compile time. With built-in search param management, data loading, code splitting, and first-class devtools, it is the most advanced routing solution available for React applications today.

TL;DR

TanStack Router delivers 100% type-safe routing for React with compile-time validation of route params, search params, loaders, and links. It features file-based routing, built-in search param serialization with Zod validation, automatic code splitting, route-level data loading, structural sharing for search params, first-class devtools, and SSR support via TanStack Start. It replaces React Router with a superior developer experience and zero runtime routing errors.

Key Takeaways
  • TanStack Router provides end-to-end type safety: route params, search params, loaders, context, and Link components are all fully typed at compile time.
  • File-based routing with automatic route tree generation eliminates manual route configuration while preserving full type inference.
  • Built-in search param management with Zod validation replaces the need for external state libraries for URL-based state.
  • Route loaders with automatic caching, invalidation, and deduplication provide a data-fetching layer similar to Remix or Next.js.
  • Code splitting is automatic with file-based routing, lazy-loading route components, loaders, and search param validators independently.
  • TanStack Router Devtools provide visual route inspection, search param debugging, and cache monitoring during development.

What Is TanStack Router and Why Type-Safe Routing?

TanStack Router is a client-side routing library for React created by Tanner Linsley (the creator of TanStack Query). It was built from the ground up with TypeScript as a first-class citizen, meaning every aspect of your routing is validated by the compiler. Route paths, path parameters, search parameters, loader data, and even Link href props are all type-checked.

Traditional routers like React Router use string-based paths and provide no compile-time guarantees. A typo in a route path or a missing search parameter only surfaces at runtime. TanStack Router eliminates this entire class of bugs by encoding your route tree into the TypeScript type system.

# Install TanStack Router
npm install @tanstack/react-router

# Install the Vite plugin for file-based routing
npm install -D @tanstack/router-plugin @tanstack/router-devtools

# 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(),
  ],
});

TanStack Router vs React Router vs Next.js Routing

Understanding how TanStack Router compares to other routing solutions helps you decide when to adopt it. Here is a feature-by-feature comparison:

FeatureTanStack RouterReact Router v7Next.js App Router
Type-safe paramsFull compile-timePartial (loader types)None (string-based)
Type-safe search paramsFull with Zod validationManual parsingManual parsing
Type-safe navigationLink props fully typedString-based pathsString-based paths
Data loadingRoute loaders with cachingLoaders (Remix-style)Server Components / fetch
Code splittingAutomatic per-routeManual lazy()Automatic per-page
Search param managementFirst-class built-inuseSearchParams (basic)useSearchParams (basic)
SSR supportVia TanStack StartBuilt-in (Remix)Built-in
DevtoolsVisual route devtoolsNone built-inNone built-in

Route Tree and File-Based Routing

TanStack Router organizes routes as a tree structure. You can define routes manually using createRoute and createRouter, or use file-based routing that automatically generates the route tree from your file system. File-based routing is the recommended approach because it eliminates boilerplate and preserves full type safety.

With file-based routing, you run the TanStack Router CLI or Vite plugin, which watches your routes directory and generates a routeTree.gen.ts file. This generated file contains the complete type-safe route tree that the router uses.

// File-based routing directory structure
// src/routes/
//   __root.tsx          -> Root layout
//   index.tsx           -> / (home page)
//   about.tsx           -> /about
//   posts.tsx           -> /posts (layout)
//   posts.index.tsx     -> /posts (index)
//   posts.$postId.tsx   -> /posts/:postId
//   _auth.tsx           -> Auth layout (pathless)
//   _auth.login.tsx     -> /login
//   _auth.register.tsx  -> /register

// src/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";

export const Route = createRootRoute({
  component: () => (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/posts">Posts</Link>
        <Link to="/about">About</Link>
      </nav>
      <Outlet />
    </div>
  ),
});
// Manual route tree definition (alternative to file-based)
import {
  createRouter,
  createRootRoute,
  createRoute,
} from "@tanstack/react-router";

const rootRoute = createRootRoute({
  component: RootLayout,
});

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/",
  component: HomePage,
});

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "posts",
  component: PostsLayout,
});

const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: "\$postId",
  component: PostDetail,
});

const routeTree = rootRoute.addChildren([
  indexRoute,
  postsRoute.addChildren([postRoute]),
]);

const router = createRouter({ routeTree });

Route Params and Search Params (Type-Safe)

Route parameters (path segments like /posts/$postId) are automatically typed based on your route definitions. There is no need for manual type assertions or runtime casts. When you access params in a loader or component, TypeScript knows the exact shape.

// src/routes/posts.$postId.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/\$postId")({
  loader: async ({ params }) => {
    // params.postId is typed as string automatically
    const post = await fetchPost(params.postId);
    return { post };
  },
  component: PostDetail,
});

function PostDetail() {
  const { post } = Route.useLoaderData();
  const { postId } = Route.useParams();
  // postId is typed as string, post is typed from loader
  return <h1>{post.title}</h1>;
}

Type-Safe Search Params with Validation

Search parameters are the killer feature of TanStack Router. Instead of manually parsing URLSearchParams, you define a validation schema (typically with Zod) and TanStack Router handles serialization, deserialization, and type inference automatically. Search params are structurally shared, meaning only changed values trigger re-renders.

// src/routes/posts.index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

const postsSearchSchema = z.object({
  page: z.number().default(1),
  sort: z.enum(["newest", "oldest", "popular"]).default("newest"),
  filter: z.string().optional(),
  tags: z.array(z.string()).default([]),
});

export const Route = createFileRoute("/posts/")({
  validateSearch: postsSearchSchema,
  component: PostsList,
});

function PostsList() {
  // All search params are fully typed!
  const { page, sort, filter, tags } = Route.useSearch();
  // page: number, sort: "newest" | "oldest" | "popular"
  // filter: string | undefined, tags: string[]

  return (
    <div>
      <Link
        to="/posts"
        search={{ page: page + 1, sort, filter, tags }}
      >
        Next Page
      </Link>
    </div>
  );
}

Loaders and Data Loading

TanStack Router has a built-in data loading system similar to Remix loaders. Each route can define a loader function that runs before the route component renders. Loaders receive typed route context, params, and search params. They support caching, invalidation, and deduplication out of the box.

Loaders integrate seamlessly with TanStack Query. You can use ensureQueryData in your loader to prefetch data that your components consume via useQuery, giving you the best of both worlds: route-level prefetching and component-level cache management.

// Route loader with TanStack Query integration
import { createFileRoute } from "@tanstack/react-router";
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";

const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ["post", postId],
    queryFn: () => fetchPost(postId),
  });

export const Route = createFileRoute("/posts/\$postId")({
  loader: async ({ params, context }) => {
    // Prefetch the data into TanStack Query cache
    await context.queryClient.ensureQueryData(
      postQueryOptions(params.postId)
    );
  },
  component: PostDetail,
});

function PostDetail() {
  const { postId } = Route.useParams();
  // Data is already cached from the loader
  const { data: post } = useSuspenseQuery(
    postQueryOptions(postId)
  );
  return <h1>{post.title}</h1>;
}

Route Context

Route context lets you pass typed data down through the route tree. Unlike React context, route context is defined per route and flows from parent routes to child routes. This is ideal for passing authentication state, feature flags, or shared services.

You define context at the router level with createRouter and extend it at each route level with beforeLoad. Child routes receive the accumulated context from all parent routes, fully typed.

// Define router context with auth and queryClient
import { createRouter } from "@tanstack/react-router";
import { QueryClient } from "@tanstack/react-query";

interface RouterContext {
  queryClient: QueryClient;
  auth: { user: User | null; isAuthenticated: boolean };
}

const router = createRouter({
  routeTree,
  context: {
    queryClient: new QueryClient(),
    auth: undefined!,  // Will be provided in <RouterProvider>
  },
});

// In your app entry point
function App() {
  const auth = useAuth();
  return (
    <RouterProvider
      router={router}
      context={{ auth }}
    />
  );
}

Layout Routes and Outlets

Layout routes wrap child routes with shared UI like navigation bars, sidebars, or footers. In TanStack Router, a layout route renders its component with an Outlet that displays the currently matched child route. File-based routing uses underscore-prefixed directories or pathless route files to define layouts.

The Outlet component in TanStack Router works similarly to React Router but with full type safety for the rendered child route.

// src/routes/posts.tsx - Layout route for /posts/*
import { createFileRoute, Outlet, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/posts")({
  component: PostsLayout,
});

function PostsLayout() {
  return (
    <div style={{ display: "flex" }}>
      <aside>
        <h2>Posts</h2>
        <nav>
          <Link to="/posts" search={{ sort: "newest" }}>
            All Posts
          </Link>
          <Link to="/posts" search={{ sort: "popular" }}>
            Popular
          </Link>
        </nav>
      </aside>
      <main>
        {/* Child routes render here */}
        <Outlet />
      </main>
    </div>
  );
}

// src/routes/_auth.tsx - Pathless layout (no URL segment)
import { createFileRoute, Outlet } from "@tanstack/react-router";

export const Route = createFileRoute("/_auth")({
  component: () => (
    <div style={{ maxWidth: 400, margin: "0 auto" }}>
      <h1>Welcome</h1>
      <Outlet />
    </div>
  ),
});

Navigation: Link and useNavigate

TanStack Router provides a type-safe Link component and a useNavigate hook. The Link component validates the to prop, params, and search params at compile time. If you link to a route that requires a postId param and forget to provide it, TypeScript catches the error immediately.

The useNavigate hook provides programmatic navigation with the same type-safety guarantees. You can navigate to routes, update search params, or replace history entries, all with full autocompletion and validation.

// Type-safe Link component
import { Link, useNavigate } from "@tanstack/react-router";

function Navigation() {
  const navigate = useNavigate();

  return (
    <div>
      {/* TypeScript validates all props */}
      <Link to="/posts/\$postId" params={{ postId: "123" }}>
        View Post
      </Link>

      {/* Search params are type-checked too */}
      <Link
        to="/posts"
        search={{ page: 1, sort: "newest" }}
        activeProps={{ style: { fontWeight: "bold" } }}
      >
        Posts
      </Link>

      {/* Programmatic navigation */}
      <button
        onClick={() =>
          navigate({
            to: "/posts/\$postId",
            params: { postId: "456" },
            search: { tab: "comments" },
          })
        }
      >
        Go to Post
      </button>

      {/* Update only search params (keep current route) */}
      <button
        onClick={() =>
          navigate({
            search: (prev) => ({ ...prev, page: prev.page + 1 }),
          })
        }
      >
        Next Page
      </button>
    </div>
  );
}

Route Guards and Authentication

TanStack Router handles authentication and route protection through the beforeLoad hook. This function runs before the route loader and component, making it the ideal place to check auth state, redirect unauthenticated users, or validate permissions.

Because beforeLoad has access to the full route context, you can pass authentication services through route context and use them in any route guard without prop drilling or global state.

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/_authenticated")({
  beforeLoad: async ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: "/login",
        search: { redirect: location.href },
      });
    }
  },
  component: () => <Outlet />,
});

// src/routes/_authenticated.dashboard.tsx
// This route is automatically protected by the parent guard
export const Route = createFileRoute("/_authenticated/dashboard")({
  component: Dashboard,
});

function Dashboard() {
  // context.auth.user is guaranteed to exist here
  return <h1>Dashboard</h1>;
}

Code Splitting

TanStack Router supports automatic code splitting with file-based routing. By creating a .lazy.tsx file alongside your route definition, the router automatically lazy-loads the component, keeping the critical route definition (params, search params, loaders) in the main bundle while deferring the component code.

This approach splits not just components but also loaders and search param validators independently, giving you fine-grained control over bundle sizes.

// src/routes/posts.$postId.tsx - Critical route config (main bundle)
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/\$postId")({
  // Loader stays in main bundle for prefetching
  loader: async ({ params }) => fetchPost(params.postId),
});

// src/routes/posts.$postId.lazy.tsx - Lazy component (separate chunk)
import { createLazyFileRoute } from "@tanstack/react-router";

export const Route = createLazyFileRoute("/posts/\$postId")({
  component: PostDetail,
  pendingComponent: () => <div>Loading post...</div>,
  errorComponent: ({ error }) => (
    <div>Error: {error.message}</div>
  ),
});

function PostDetail() {
  const post = Route.useLoaderData();
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

TanStack Router Devtools

TanStack Router includes a visual devtools panel that displays the current route tree, active route matches, search params state, loader cache, and pending navigations. The devtools are invaluable for debugging complex routing scenarios and understanding data flow.

The devtools are tree-shakable and only included in development builds. You add them with a single component that automatically disappears in production.

// Add devtools to your app
import { TanStackRouterDevtools } from "@tanstack/router-devtools";

// In your root route component
export const Route = createRootRoute({
  component: () => (
    <>
      <Outlet />
      {/* Only rendered in development */}
      <TanStackRouterDevtools position="bottom-right" />
    </>
  ),
});

SSR Integration with TanStack Start

TanStack Start is the full-stack framework built on TanStack Router that adds server-side rendering, server functions, and deployment adapters. It provides the same type-safe routing on the server, enabling features like server-side data loading, streaming SSR, and API routes.

If you need SSR, TanStack Start is the official solution. For client-side single-page applications, TanStack Router works standalone with Vite and any bundler.

// TanStack Start - app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tsConfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  vite: {
    plugins: [tsConfigPaths()],
  },
  server: {
    preset: "node-server",  // or vercel, netlify, cloudflare
  },
});

// Server function example
import { createServerFn } from "@tanstack/react-start";

const getServerTime = createServerFn()
  .validator(z.object({ timezone: z.string() }))
  .handler(async ({ data }) => {
    return new Date().toLocaleString("en-US", {
      timeZone: data.timezone,
    });
  });

Migration from React Router

Migrating from React Router to TanStack Router can be done incrementally. The key changes involve replacing Route components with createRoute calls (or file-based route files), converting useParams/useSearchParams to the typed equivalents, and replacing Link components. The route structure concepts (nested routes, outlets, layouts) map directly.

Start by installing TanStack Router alongside React Router and migrating one route branch at a time. The TanStack Router Vite plugin and route tree generator handle most of the configuration automatically.

// BEFORE: React Router v6
import { BrowserRouter, Routes, Route, useParams } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/posts" element={<PostsLayout />}>
          <Route index element={<PostsList />} />
          <Route path=":postId" element={<PostDetail />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function PostDetail() {
  const { postId } = useParams(); // postId: string | undefined
  // No type safety!
}

// AFTER: TanStack Router (file-based)
// src/routes/__root.tsx
// src/routes/index.tsx
// src/routes/posts.tsx
// src/routes/posts.index.tsx
// src/routes/posts.$postId.tsx

// Each file exports a typed Route - params are guaranteed

Search Param Validation with Zod

Zod is the recommended validation library for TanStack Router search params. You define a Zod schema in your route definition, and TanStack Router automatically validates, serializes, and types your search params. Invalid search params are caught and can be handled with fallback defaults.

The search param validation runs on every navigation, ensuring your components always receive valid data. Combined with structural sharing, only the search params that actually changed trigger component re-renders.

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

const productSearchSchema = z.object({
  category: z.enum(["electronics", "clothing", "books"]).catch("electronics"),
  priceMin: z.number().min(0).catch(0),
  priceMax: z.number().max(10000).catch(10000),
  inStock: z.boolean().catch(true),
  q: z.string().optional(),
});

type ProductSearch = z.infer<typeof productSearchSchema>;

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

function ProductsPage() {
  const search = Route.useSearch();
  // search is fully typed as ProductSearch
  // Invalid URL params are caught and replaced with defaults

  return (
    <div>
      <Link
        to="/products"
        search={(prev) => ({ ...prev, category: "books" })}
      >
        Books
      </Link>
    </div>
  );
}

Pending and Error States

TanStack Router provides built-in support for pending UI (loading states during navigation) and error boundaries at the route level. Each route can define a pendingComponent that shows during data loading and an errorComponent that renders when loaders fail.

The pendingMs and pendingMinMs options let you control when the pending UI appears and how long it stays visible, preventing flash-of-loading-state for fast navigations.

// src/routes/posts.$postId.tsx
export const Route = createFileRoute("/posts/\$postId")({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId);
    if (!post) throw new Error("Post not found");
    return { post };
  },
  // Show after 1s of loading (avoid flash for fast loads)
  pendingMs: 1000,
  // Show for at least 500ms once visible
  pendingMinMs: 500,
  pendingComponent: () => (
    <div style={{ padding: "2rem", textAlign: "center" }}>
      <span>Loading post...</span>
    </div>
  ),
  errorComponent: ({ error, reset }) => (
    <div style={{ padding: "2rem", color: "red" }}>
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  ),
  component: PostDetail,
});

Best Practices

Follow these best practices to get the most out of TanStack Router in production applications.

  • Use file-based routing for automatic code splitting, route tree generation, and consistent project structure.
  • Define search param schemas with Zod for every route that uses URL state. This ensures type safety and automatic validation.
  • Use route context to pass authentication state and shared services instead of global React context for route-specific data.
  • Leverage structural sharing in search params to prevent unnecessary re-renders when only some params change.
  • Split route components into .lazy.tsx files to keep the main bundle small and defer heavy component code.
  • Integrate TanStack Query with route loaders using ensureQueryData for optimistic prefetching with component-level cache control.
  • Use beforeLoad for authentication guards instead of wrapping components, as it runs before any rendering or data loading.
  • Configure pendingMs (default 1000ms) and pendingMinMs to avoid flash-of-loading for fast navigations while showing feedback for slow ones.
  • Keep the TanStack Router Devtools enabled during development for visual debugging of route matches, params, and cache state.
  • Use the router.invalidate() method to refetch loader data after mutations instead of manually managing cache invalidation.

Frequently Asked Questions

What is TanStack Router?

TanStack Router is a fully type-safe client-side routing library for React. It provides compile-time validation of route paths, params, search params, loaders, and navigation links. Created by Tanner Linsley, it is part of the TanStack ecosystem alongside TanStack Query, TanStack Table, and TanStack Form.

How is TanStack Router different from React Router?

TanStack Router provides end-to-end type safety that React Router lacks. Route params, search params, and Link components are fully typed at compile time. TanStack Router also includes built-in search param management with Zod validation, automatic code splitting, route-level data caching, and visual devtools. React Router uses string-based paths with no compile-time validation of params or links.

Do I need TanStack Start to use TanStack Router?

No. TanStack Router works as a standalone client-side routing library with Vite or any bundler. TanStack Start is only needed if you want server-side rendering, server functions, or a full-stack framework. For single-page applications, TanStack Router alone is sufficient.

How do search params work in TanStack Router?

Search params in TanStack Router are defined with a validation schema (typically Zod) in the route definition. The router automatically serializes objects to URL search strings and deserializes them back with full type safety. Structural sharing ensures components only re-render when the specific search params they use actually change.

Can I migrate from React Router to TanStack Router incrementally?

Yes. You can install TanStack Router alongside React Router and migrate one route branch at a time. The route concepts (nested routes, outlets, layouts) map directly between the two libraries. Start with leaf routes and work upward, converting Route components to file-based route definitions.

Does TanStack Router support code splitting?

Yes. With file-based routing, code splitting is automatic. Create a .lazy.tsx file alongside your route definition, and the router lazy-loads the component while keeping the route configuration (params, search params, loaders) in the main bundle. This gives you optimal bundle splitting without manual lazy() calls.

How does data loading work in TanStack Router?

Each route can define a loader function that runs before the component renders. Loaders receive typed params, search params, and route context. They support caching, automatic invalidation, and deduplication. For advanced caching, you can integrate TanStack Query by calling ensureQueryData in the loader.

Is TanStack Router production-ready?

Yes. TanStack Router has reached stable v1 status and is used in production by companies of all sizes. It has comprehensive documentation, active maintenance, and a growing ecosystem. The library is battle-tested with the same quality standards as TanStack Query, which is used by millions of developers.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

{ }JSON FormatterTSJSON to TypeScript%20URL Encoder/Decoder

บทความที่เกี่ยวข้อง

React Design Patterns Guide: Compound Components, Custom Hooks, HOC, Render Props & State Machines

Complete React design patterns guide covering compound components, render props, custom hooks, higher-order components, provider pattern, state machines, controlled vs uncontrolled, composition, observer pattern, error boundaries, and module patterns.

React Query Patterns 2026: Data Fetching, Caching และ Mutations ด้วย TanStack Query

เชี่ยวชาญ React Query (TanStack Query) patterns 2026: useQuery, useMutation, optimistic updates

Remix Complete Guide: Full-Stack Web Framework with Web Standards (2026)

Comprehensive Remix guide covering loaders, actions, nested routes, error boundaries, streaming, resource routes, authentication, deployment, and migration from Next.js.