mrkeyoor.com_
Sun 20 Sept 15:53 UTC
npmWeb Frontendupdated 20 Sept 2026

@tanstack/react-router review

@tanstack/react-router 1.170.31 produced a 36.2 KB gzipped all-package browser bundle in our sandbox and loaded through both ESM import and require(). The current 1.170.32 package routes React through a TypeScript-known tree, generated from files or assembled in code. That tree types destinations, params, search state, loader data, and context. Routes can validate URLs, preload data, guard entry, and render pending, error, or not-found states. It remains a client router; TanStack Start supplies server rendering and server functions.

Verdict

@tanstack/react-router 1.170.31 installed in 2.5 seconds, used 15 MB across 13 packages, and bundled to 36.2 KB gzipped in our sandbox with 0 audit findings. It earns that setup in a TypeScript SPA with typed URLs and loaders; simple JavaScript navigation or server rendering calls for a smaller router or full framework.

We installed it

Lab card: what happened when we installed @tanstack/react-routerScreenshot of @tanstack/react-router documentation
Install✓ · 2.5s13 packages on disk · 15 MB
ImportESM import works · require() works · ESM package with exports map
Browser36.2 KBgzipped (106.9 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does @tanstack/react-router install cleanly?

Yes. In a fresh container with an empty cache, npm install @tanstack/react-router finished in 3 seconds, leaving 13 packages and 15 MB on disk. npm audit reported no known vulnerabilities.

How much does @tanstack/react-router add to a browser bundle?

36.2 KB gzipped (106.9 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does @tanstack/react-router work with both ESM and CommonJS?

Yes. Both import '@tanstack/react-router' and require('@tanstack/react-router') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does @tanstack/react-router include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

@tanstack/react-router or react-router: which should you use?

react-router: Choose it for established data-router conventions, a long history of third-party examples, and framework or SSR modes. @tanstack/react-router 1.170.31 installed in 2.5 seconds, used 15 MB across 13 packages, and bundled to 36.2 KB gzipped in our sandbox with 0 audit findings.

When should you not use @tanstack/react-router?

Skip it for plain JavaScript when compile-time route inference cannot repay the generated-tree and registration setup.

API stability4/5The 1.x contract still centers on createRouter, createFileRoute, a route tree, registered router types, typed links, hooks, loaders, and beforeLoad. Registry release 1.170.32 sits 1 patch beyond our measurement, reflecting a rapid publish cadence. Semver bounds ordinary updates, but generator output and build-plugin behavior expose more moving parts than a runtime-only router.
Docs5/5The official site separates file and code routing, then documents search validation, context, authentication, data loading, testing, rendering, and every public API. Its type-safety guide names expensive inference shapes and shows how from or narrower loader returns reduce them. The volume can bury a basic answer, but configuration and performance costs are disclosed with working code.
Maintenance5/5GitHub shows the unarchived TanStack monorepo pushed on August 26, 2026, with 585 open issues and pull requests across Router and Start. npm lists 1.170.32 as current, and package releases plus bundler plugins continue regularly. The shared tracker prevents the open count from describing React Router alone, but current code and publishing activity are clear.
Ecosystem4/5npm counted 23,569,505 downloads from August 19 through 25, 2026, and GitHub reports 14,992 stars. TanStack maintains the generator, current bundler plugins, devtools, validation adapters, Start, and Query integration. React Router retains more third-party history, while this package's closest integrations sit inside TanStack's TypeScript-focused family.

Use it if

  • Use it when a TypeScript SPA should reject invalid destinations or missing route params during compilation.
  • Choose it when query-string values need route-specific parsing, defaults, validation, and typed updates.
  • Adopt it when loaders and intent preloading should live beside routes and share TanStack Query's cache.
  • Use it for nested client layouts that need local pending, error, and not-found boundaries without a server framework.
Skip it if

Setup reality

We installed @tanstack/react-router 1.170.31 in 2.5 seconds in a fresh Node 22 container. It left 13 packages using 15 MB, with 4 direct dependencies and 2 peers: React and React DOM. The package itself was 2,040 KB unpacked and requires Node 20.19 or newer. It is ESM with an exports map, yet require() and ESM import both worked. Types are bundled. npm audit reported 0 findings. A full import bundled to 106.9 KB minified and 36.2 KB gzipped. The registry is now at 1.170.32.

File routes require @tanstack/router-plugin in the build. Under Vite, tanstackRouter() must run before the React transform. It watches src/routes and writes src/routeTree.gen.ts. The documented workflow commits that output while excluding it from edits, lint, and formatting. Filenames carry structure: __root names the root, $name creates a parameter, a leading _ creates a pathless layout, and a trailing _ escapes a parent layout.

createRouter consumes the tree, and TypeScript module augmentation registers typeof router with @tanstack/react-router. Runtime navigation still works without that declaration, but route-aware types largely disappear. Search state also needs a validator and a defined response to invalid values. Zod 4 schemas work directly; older Zod integrations may require the TanStack adapter. Decide whether bad input receives a default or reaches an error component.

Router loaders cache on their own terms and do not replace every server-state cache. With TanStack Query, loaders can ensure query data while components read the identical options. Add from to relative links and search changes so TypeScript checks one route rather than the whole tree union. beforeLoad guards navigation UI, never the backing API. Test code splitting, preload timing, stale times, and pending delays on a slow connection.

Patterns

Run the route generator before React in Vite vite-plugin-setup

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({ target: 'react', autoCodeSplitting: true }),
    react(),
  ],
})

Plugin order is functional: the router transform precedes React. Its defaults already read src/routes and write src/routeTree.gen.ts.

Build a router and connect its generated type create-and-register-router

// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

Register.router supplies the generated type to Link and route hooks. Removing the augmentation leaves navigation alive while losing route-aware checks.

Render navigation and child routes at the root root-route

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

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/">Home</Link> <Link to="/posts">Posts</Link>
      </nav>
      <Outlet />
    </>
  ),
})

// src/main.tsx
// <RouterProvider router={router} />

Every destination includes this root component. Outlet marks the child insertion point, while conditional shells belong in a pathless child layout.

Fetch a post before rendering its route route-with-loader

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

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostPage,
})

function PostPage() {
  const post = Route.useLoaderData()
  const { postId } = Route.useParams()
  return <article>{post.title}</article>
}

File generation depends on the named Route export and controls the createFileRoute path. Its params and loader result become types for these hooks.

Build typed links for a post and next page typed-links

import { Link } from '@tanstack/react-router'

<Link to="/posts/$postId" params={{ postId: '42' }}>
  Read post
</Link>

<Link from={Route.fullPath} to="." search={(prev) => ({ ...prev, page: prev.page + 1 })}>
  Next page
</Link>

from anchors the relative search update to one route, avoiding a TypeScript comparison against every search schema.

Parse product filters from the URL validate-search-params

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

const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(''),
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})

export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
  component: Products,
})

Zod catch values replace invalid inputs with defaults. Throwing validation instead reaches the route error component; Zod 3 may need the adapter, while Zod 4 works directly.

Increment one search parameter read-and-update-search

function Products() {
  const { page, filter } = Route.useSearch()
  const navigate = Route.useNavigate()

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

The callback receives the current search object, so spreading prev keeps filter and sort. A new object drops omitted values.

Redirect unauthenticated navigation from a layout auth-guard

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

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    if (!(await isAuthenticated())) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
})

beforeLoad must throw the redirect before child guards run. The API still needs its own authorization because this check controls client navigation.

Expose QueryClient to route loaders router-context

// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'

export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
  component: () => <Outlet />,
})

// src/router.tsx
const router = createRouter({ routeTree, context: { queryClient } })

createRootRouteWithContext uses 2 calls: the first declares context, and the second configures the route. Descendant loaders inherit that type.

Warm a query in the loader and read it in React query-integration

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ context: { queryClient }, params: { postId } }) => {
    await queryClient.ensureQueryData(postQueryOptions(postId))
  },
  component: Post,
})

function Post() {
  const { postId } = Route.useParams()
  const { data } = useSuspenseQuery(postQueryOptions(postId))
  return <h1>{data.title}</h1>
}

The loader awaits cache population without returning the data, keeping its inferred type small. useSuspenseQuery then reads the identical query key.

Handle 3 route outcomes beside the loader error-and-not-found

import { createFileRoute, notFound } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw notFound()
    return post
  },
  pendingComponent: () => <Spinner />,
  errorComponent: ({ error, reset }) => (
    <button onClick={reset}>{String(error)}</button>
  ),
  notFoundComponent: () => <p>No such post</p>,
})

Thrown notFound bubbles to the nearest matching handler. This route also contains its loading and ordinary error UI.

Read route data without importing the route module route-api-outside-route-file

import { getRouteApi } from '@tanstack/react-router'

const routeApi = getRouteApi('/posts/$postId')

export function PostSidebar() {
  const post = routeApi.useLoaderData()
  const { postId } = routeApi.useParams()
  return <aside>{postId}</aside>
}

getRouteApi avoids a circular route-module import while retaining typed hooks. The generated tree checks the supplied /posts/$postId path.

Alternatives

PackageRegistryPick it when
react-routernpmChoose it for established data-router conventions, a long history of third-party examples, and framework or SSR modes.
wouternpmChoose it for a small client app that wants matching and hooks without generators or a loader system.
@tanstack/react-startnpmChoose it when TanStack Router should run inside a framework that owns SSR and server functions.
nextnpmChoose it when routing, React Server Components, rendering strategy, and deployment should be one framework decision.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.