← Back to blog
··14 min read

React Router vs TanStack Router vs Wouter in 2026: Which Client-Side Router Should Your React App Use

React RouterTanStack RouterWouterRoutingReactViteTypeScriptSPA
React Router vs TanStack Router vs Wouter in 2026: Which Client-Side Router Should Your React App Use

Routing Is the One Layer Every SPA Has to Get Right

Every single-page app makes the same early decision, usually without much ceremony: how do URLs map to what the user sees? Load one HTML shell, hand control to JavaScript, and something has to watch the address bar, decide that /dashboard shows the dashboard and /settings/billing shows the billing panel, update the URL when the user clicks a link, and keep the back button, bookmarking, and deep links working. That something is a client-side router, and it is one of the few pieces of infrastructure that touches almost every screen in the app.

Get it right and you barely notice it — links work, layouts nest cleanly, refreshing a deep URL lands on the right view. Get it wrong and you feel it everywhere: broken back buttons, un-shareable URLs, a tangle of manual history management, and query strings parsed by hand in ten different components. Because routing is so pervasive, the router you choose shapes how the rest of the app is written far more than its small footprint suggests.

One scoping note before the comparison, because it removes a lot of confusion: if you are on a meta-framework, routing is already decided. Next.js, Remix, and TanStack Start ship file-based routing — you make files, not install a router. The React Router vs TanStack Router vs Wouter question is specifically for React apps that own their own routing: the classic Vite SPA, a dashboard shipped as a static bundle, an embedded app, a browser extension, an Electron UI. If a framework owns your routing, use what it gives you. If you own it, read on.

In 2026 the three names that dominate that conversation are React Router, TanStack Router, and Wouter. They get compared as rivals, but like most tooling trios they are not identical — they sit at different points on a single spectrum: *how much does the router do beyond matching URLs, how strongly does it type your routes, and how many kilobytes does it ship?*

  • React Router is the ubiquitous default that is now also a framework: the router almost everyone knows, with v7 folding in loaders, actions, and pending UI. It answers _"I want the proven option everyone understands, and room to grow."_
  • TanStack Router is the type-safe, data-first router: fully typed routes and validated search params, with a built-in loader and caching layer. It answers _"I want typed routing and data loading to remove real bugs."_
  • Wouter is the tiny router that stays out of your way: ~2 KB, hook-based, no data layer, no build step. It answers _"I want routing and nothing else."_
  • Put simply: React Router fits almost anything, TanStack Router fits complex typed apps, and Wouter fits small ones. The rest of this guide matches each to the question you actually have.

    Code on a screen representing the routing layer that maps URLs to views

    Code on a screen representing the routing layer that maps URLs to views

    Three Philosophies of Routing

    Before comparing features, it helps to see the three philosophies underneath, because they explain every trade-off that follows.

    React Router: the pragmatic incumbent that grew into a framework. React Router has been the default React router for years, which means the largest documentation footprint, the most Stack Overflow answers, and the highest odds that any developer you hire already knows it. Version 7 was the pivotal release: it merged the Remix data layer — loader functions for fetching, action functions for mutations, and navigation state for pending and optimistic UI — directly into React Router. The result is a library that can be a plain SPA router when you want simplicity, or a full-stack, data-loading framework when you want more, without changing tools. The trade: type safety is good but you opt into more of it, and the API has evolved across major versions, so older tutorials can mislead.

    TanStack Router: type-first and data-first from day one. TanStack Router did not grow into type safety — it was built around it. Route definitions produce fully inferred params, links are checked so you cannot navigate to a route that does not exist, and its standout feature is typed, schema-validated URL search params: ?page=2&sort=name is parsed through a schema and handed to your components as typed values, not untyped strings. It ships a built-in loader and caching system with hover-preloading, and it is designed to pair with TanStack Query. The trade: a newer ecosystem and more setup up front than a router you can drop in and forget.

    Wouter: minimalism as the whole point. Wouter's model is different in kind. It is roughly 2 KB, hook-first (useLocation, useRoute), with no data layer, no code generation, and no build step. It does route matching, links, and history — and stops there. The result is a router that essentially disappears from your bundle and your mental overhead. The trade: no built-in data loading and only light type inference, so as the app grows you feel the missing features.

    The reason this matters: a do-everything router removes boilerplate but asks you to learn its model; a type-first router prevents bugs but costs setup; a tiny router gets out of your way but leaves the bigger jobs to you. Picking the wrong shape means either fighting a heavy router in a small app, or hand-rolling features a proper router would have given you.

    React Router: The Ubiquitous Default

    React Router is the default answer when you want the proven option that every React developer already understands and that can scale from a two-page SPA to a full data-loading app. Its central advantage is ubiquity plus range: you are unlikely to hit a routing problem someone has not already solved and documented, and v7 means you will not outgrow it.

    tsx
    // v7 data router: routing and data fetching in one place
    import { createBrowserRouter, RouterProvider } from "react-router";
    
    const router = createBrowserRouter([
      {
        path: "/orders",
        loader: async () => fetchOrders(), // runs before the route renders
        Component: Orders,
      },
    ]);
    
    export default function App() {
      return <RouterProvider router={router} />;
    }

    Because a route's loader runs before the component renders, data fetching is tied to the URL — navigating to /orders triggers the orders loader — which centralizes fetching, loads nested routes in parallel, and avoids the classic "render, then useEffect, then spinner" waterfall. The router also exposes navigation state, so pending UI, skeletons, and optimistic updates are first-class rather than something you wire up by hand.

    TypeScript support in v7 is genuinely good: route typegen gives you typed loader data and params, so you are far from untyped. It is simply not built from the ground up so that an invalid route or an unvalidated search param is a compile error — you get there with more of your own care than a type-first router demands.

    Best when: you want the router everyone knows, the deepest well of answers when something breaks, and one library that starts simple and grows into data loading without a rewrite.

    TanStack Router: Type-Safe and Data-First

    TanStack Router is the default answer when end-to-end type safety and serious data loading are the point — most often a complex dashboard or a data-heavy SPA. Its defining feature is that routes, params, and search params are fully typed and validated.

    tsx
    import { createFileRoute } from "@tanstack/react-router";
    import { z } from "zod";
    
    export const Route = createFileRoute("/orders")({
      // search params are parsed + validated + typed
      validateSearch: z.object({ page: z.number().default(1) }),
      loader: () => fetchOrders(),
      component: Orders,
    });
    
    function Orders() {
      const { page } = Route.useSearch(); // page is a typed number, not a string
      return <OrderList page={page} />;
    }

    That validateSearch block is the headline: ?page=2 is parsed through a Zod schema and handed to your component as a typed number, so a whole category of bugs — mistyped query keys, forgotten parsing, invalid URL states — becomes a compile-time or validation error instead of a runtime surprise. Links are typed too, so you cannot navigate to a route that does not exist or forget a required param.

    On data, TanStack Router ships a built-in loader with caching and hover-preloading (it can prefetch a route's data when the user hovers a link, so navigation feels instant), and it is designed to integrate tightly with TanStack Query — many teams use Router for the URL-and-loader layer and Query for the cache-and-mutation layer, with types flowing between them. It supports file-based or code-based routes, and if you later want server-side rendering, TanStack Start is the meta-framework built directly on top of it — a natural upgrade path.

    Best when: you are building a complex, data-heavy SPA where typed search params and built-in loaders remove real bugs and boilerplate, and you are willing to trade a newer ecosystem and more setup for that rigor.

    Developer workstation with code and terminal, representing typed routes and a built-in data loader

    Developer workstation with code and terminal, representing typed routes and a built-in data loader

    Wouter: The Tiny Router That Stays Out of the Way

    Wouter is the default answer when the app is small and you want routing without ceremony. Its model is minimalism: around 2 KB, hook-first, no data layer, no build step.

    tsx
    import { Route, Link, Switch } from "wouter";
    
    function App() {
      return (
        <Switch>
          <Route path="/" component={Home} />
          <Route path="/users/:id">{(params) => <User id={params.id} />}</Route>
          <Route>404 — Not Found</Route>
        </Switch>
      );
    }

    That is essentially the whole API surface, and for a widget, an embed, a prototype, or a small SPA that is a feature, not a limitation. Wouter gives you route matching, Link, and history handling in a package small enough to be a rounding error in your bundle, with nothing to configure and nothing to learn beyond a couple of hooks (useLocation, useRoute).

    The cost is scope. There is no loader concept — you fetch data inside components the way you would without a router, typically with TanStack Query, SWR, or useEffect — and type inference is light: route params come back loosely typed, and there is no search-param validation because that is outside its tiny remit. For a small app, "the router routes, the data library fetches" is a perfectly clean separation. For a large one, you will eventually miss the features the bigger routers include.

    Best when: the app is small to medium, every kilobyte and every line of config counts, and you would rather keep routing and data fetching as separate concerns.

    Head to Head: The Decisions That Actually Differ

    Type safety. TanStack Router wins clearly on the axis it was designed around — typed params and schema-validated search params make invalid routes and unparsed query strings compile-time or validation errors. React Router v7 is a strong second with typegen for loaders and params. Wouter is lightest by design.

    Data loading and pending states. React Router and TanStack Router both couple fetching to routes with first-class pending UI; TanStack Router adds typed loader data and hover-preloading. Wouter does none of this on purpose — bring your own data-fetching library.

    Bundle size. Wouter's ~2 KB is a real edge at the small end. In a dashboard, the few kilobytes between React Router and TanStack Router are noise next to the features they add — choose on capability there, not size.

    Search params. TanStack Router treats the URL query string as typed, validated application state — a genuine differentiator for filter-and-table-heavy apps. React Router gives you useSearchParams (untyped strings you parse). Wouter leaves it to you.

    Ecosystem and familiarity. React Router is the clear leader — the most downloads, the most tutorials, the highest odds a new hire already knows it. TanStack Router is newer but growing fast alongside the rest of the TanStack suite. Wouter is small, stable, and beloved by the minimalists who use it.

    Growth path. React Router v7 grows from SPA router to full framework in place. TanStack Router grows into TanStack Start for SSR. Wouter deliberately does not grow — if you outgrow it, you migrate.

    Which Should You Ship?

    A typical Vite SPA → React Router. It is the router everyone knows, the documentation is everywhere, and v7 means you can start simple and add loaders and data APIs later without swapping tools. For most React apps that own their routing, this is the lowest-surprise choice.

    A complex, data-heavy, or typed-to-the-hilt app → TanStack Router. If typed search params and built-in loaders would remove real bugs from a serious dashboard or analytics tool — the same instinct that makes you validate inputs with a schema and choose TypeScript over JavaScript — its rigor pays for the extra setup, especially paired with TanStack Query.

    A small, lean, or embedded app → Wouter. For a widget, an embed, a prototype, or a landing page with a few routes, its 2 KB and zero config are exactly right, and the features the bigger routers add would go unused.

    The mistake to avoid is choosing on familiarity alone in a data-heavy app (where TanStack Router's typed routing saves you real debugging) or on features alone in a tiny one (where Wouter's minimalism is the better fit). And the mistake that trumps all of these: reaching for any of them when you are on Next.js — there, the App Router already owns routing, and adding a client router fights the framework.

    Shipping Routing in a Template You Sell

    If you build templates and starters to sell, the routing layer is judged the same way buyers judge everything else in the codebase: is it familiar, easy to extend, safe by default, and documented? A few rules make a router read as production-ready:

  • Match the buyer's expectations. A general React/Vite starter should usually default to React Router because buyers recognize it; reach for TanStack Router when type safety and a data-heavy architecture are the selling point, or Wouter when "lean" is the pitch.
  • Keep routes in one place. Route definitions belong in a single, documented module — not scattered as magic-string paths through every component — so a buyer can add a route without archaeology.
  • Type your params (and search params). Typed params, and validated search params where the router supports them, mean a buyer cannot navigate to a route that does not exist — the same care that signals quality in a component library choice.
  • Include the patterns to copy. Ship a couple of nested routes and a not-found route so the buyer sees how the structure works and can extend it confidently.
  • Document the structure. One short doc — how routes are organized, how to add one, how data loads — does as much for perceived quality as the screens themselves.
  • A routing layer that is familiar, typed where possible, trivial to extend, and documented signals the same care as clean data fetching and real validation — the details that separate a template that sells from one that sits.

    The Bottom Line

    All three libraries do the core job well: map URLs to views, keep history and the back button working, and support nested layouts and deep links. The decision is not "which one does routing" — it is *how much routing machinery you need, how much you value automatic type safety, and how tight your bundle budget is*.

  • React Router — the ubiquitous default that is now also a framework: everyone knows it, the ecosystem is deepest, and v7 grows from SPA router to full data-loading framework in place.
  • TanStack Router — the type-safe, data-first router: typed routes, schema-validated search params, and a built-in loader — the strongest choice for complex, data-heavy SPAs.
  • Wouter — the tiny router that stays out of your way: ~2 KB, hook-based, no data layer — ideal for small, lean, and embedded apps.
  • Pick React Router unless you have a specific reason to reach past it — typed routing and heavy data needs pull you to TanStack Router, and a hard smallness constraint pulls you to Wouter. And if you are on a meta-framework, none of this applies: use the routing it already gives you.

    Ready to turn what you build into income? List your React or SaaS template on CodeCudos, see how routing fits the wider stack in our best tech stack for web apps in 2026 guide, pick your framework with Next.js vs Remix vs TanStack Start, pair your router with the right data-fetching library, or make sure the whole build reads as production-ready.

    Frequently asked questions

    What is a client-side router and when do I actually need one?

    A client-side router maps the URL in the browser to which React components render, without a full page reload — and you need one the moment your single-page app has more than one 'screen' that should be linkable, bookmarkable, and navigable with the back button. In a traditional multi-page site the server decides what HTML each URL returns. In a single-page app (SPA) the browser loads one HTML shell and JavaScript takes over; a router is the piece that watches the URL, decides which component tree to show for /dashboard vs /settings/billing, updates the URL when the user navigates, and keeps the browser's history, back/forward buttons, and deep links working. Without one, you either cannot have real URLs for different views (everything lives at one path, which breaks bookmarking, sharing, and SEO) or you reinvent history management by hand. A router also typically handles nested layouts (a shell that stays mounted while an inner panel changes), route parameters (/users/:id), query/search params (?tab=invoices), redirects, and 'not found' states. The nuance for 2026: if you are on a meta-framework like Next.js, Remix, or TanStack Start, routing is built in and file-based — you do not add a separate router. The React Router vs TanStack Router vs Wouter decision is specifically for React apps that own their own routing, most commonly a Vite SPA, an app embedded in a larger page, or a dashboard that ships as a static bundle. If that is you, one of these three is the layer that turns URLs into views.

    What is the core difference between React Router, TanStack Router, and Wouter?

    It comes down to three things: how much the router does beyond matching URLs, how strongly it types your routes and params, and how much code it ships — and those axes explain almost every other trade-off. React Router is the incumbent and the ubiquitous default: nearly every React developer has used it, its documentation and Stack Overflow footprint are enormous, and version 7 folded the Remix data layer (loaders for fetching, actions for mutations, and pending/optimistic UI) directly into it, so the same library can be a simple SPA router or a full-stack framework depending on how you configure it. Its type safety is good, and improving, but not automatic end-to-end the way a type-first router is. TanStack Router was built type-first from day one: your route definitions produce fully inferred, type-checked params and — its standout feature — typed, schema-validated URL search params, so ?page=2&sort=name is parsed, validated, and typed rather than being a bag of untyped strings you hand-parse. It ships a built-in loader and caching system that pairs cleanly with TanStack Query, supports file-based or code-based routes, and is aimed squarely at complex, data-heavy SPAs. Wouter is the minimalist: around 2 KB, hook-based (useLocation, useRoute), no data-loading layer, no build step, no ceremony — it does routing and nothing else, which is exactly the point for small apps. The spectrum: React Router = ubiquitous default that is also a framework, TanStack Router = type-safe and data-first, Wouter = tiny and out of your way. Pick based on how much routing machinery you need, how much you value automatic type safety, and how tight your bundle budget is.

    Which router has the best TypeScript and type safety story?

    TanStack Router has the strongest type-safety story of the three, and it is not close on the dimension it was designed around: route params and URL search params are fully inferred and validated. When you define a route, the params it exposes are typed, links to it are checked so you cannot navigate to a route that does not exist or forget a required param, and search params are parsed through a schema (often a Zod or Valibot schema) so ?page=2 becomes a typed number your components read with confidence rather than a string you parse and guard by hand. That single feature removes a whole category of bugs — mistyped query keys, forgotten parsing, invalid states that only surface at runtime — and it is the main reason data-heavy dashboards reach for it. React Router's TypeScript support is solid and has improved a lot: v7 generates types for routes and, with its typegen, gives you typed loader data and params, so it is far from untyped — but you opt into more of it, and the historical API meant more manual typing than a type-first router. In practice React Router in 2026 is a good TypeScript experience; it is just not built from the ground up so that an invalid route or an unvalidated search param is a compile error. Wouter is the lightest here by design: it has TypeScript types and works fine in a typed codebase, but it does minimal inference — route params come back loosely typed, and there is no built-in search-param validation because that is outside its tiny scope. The honest ranking on end-to-end type safety is TanStack Router first by a clear margin, React Router a strong second, Wouter third — and if typed search params and compile-time route safety would prevent real bugs in your app (the same instinct behind validating inputs with a schema), TanStack Router is purpose-built for exactly that.

    How do the routers compare on data loading and pending states?

    Data loading is where React Router and TanStack Router both do real work and Wouter deliberately does none — and the difference matters most for apps where every route fetches something. React Router v7 inherited the Remix model: each route can declare a loader that fetches its data before the route renders, an action for mutations, and the router exposes navigation state so you can show pending UI, skeletons, and optimistic updates while a transition is in flight. This means data fetching is tied to the URL — navigate to /orders and the orders loader runs — which centralizes fetching, enables parallel loading of nested routes, and avoids the classic 'render, then useEffect, then spinner' waterfall. TanStack Router has its own built-in loader system with caching and preloading (it can prefetch a route's data on link hover, so navigation feels instant), and it is designed to integrate tightly with TanStack Query, so many teams use Router for the URL-and-loader layer and Query for the cache-and-mutation layer, with types flowing between them. Both give you first-class pending states, though they express them differently. Wouter has no loader concept at all — you fetch data inside your components the way you would without a router, typically with TanStack Query, SWR, or useEffect. That is not a flaw; it is the minimalist bargain, and for many apps 'the router routes, the data library fetches' is a perfectly clean separation. The practical read: if you want fetching coupled to routes with built-in pending UI, React Router and TanStack Router both deliver, with TanStack Router adding typed loader data and hover-preloading; if you would rather keep routing and data fetching as separate concerns and bring your own data library, Wouter's do-nothing approach is a feature.

    Which is the smallest, and how much does bundle size matter?

    Wouter is by far the smallest — roughly 2 KB minified and gzipped — and that is its headline advantage; React Router and TanStack Router are both meaningfully larger because they do meaningfully more. Wouter achieves its size by being hook-first and feature-light: it gives you route matching, links, and history handling in a tiny package with no data layer, no build step, and no code generation. For a widget, an embed, a marketing microsite, or a small SPA where you are counting kilobytes, that is a real edge — the router essentially disappears from your bundle budget. React Router is a larger dependency (the exact figure depends on which parts you import, and v7's data APIs add weight if you use them), but for a typical application it is a small fraction of total JavaScript once your UI library, framework, and app code are counted, so its size is rarely the deciding factor. TanStack Router is also larger than Wouter because it ships the type machinery, the loader and caching system, and search-param validation — you are paying in bytes for features that remove code elsewhere, which is usually a good trade in the complex apps it targets. The honest way to think about it: bundle size is a real tiebreaker only at the small end, where Wouter's 2 KB genuinely matters and the bigger routers' features would go unused. In a dashboard or a data-heavy SPA, the few kilobytes of difference between React Router and TanStack Router are noise next to the bugs typed routing prevents and the boilerplate loaders remove — there, choose on features and type safety, not size. Choose on size when the app is small enough that the router's weight is a visible share of the whole.

    If I am already on Next.js or another meta-framework, do I need one of these?

    No — and this is the most important scoping point, because it saves people from adding a router they should not. Next.js (App Router or Pages Router), Remix, and TanStack Start all include their own file-based routing, and you route by creating files and folders, not by installing React Router, TanStack Router, or Wouter. Adding a separate client-side router on top of a meta-framework fights the framework: you would be duplicating routing, breaking server-side rendering and the framework's data-loading model, and confusing the very thing the framework exists to handle. So if your project is Next.js, the routing decision is already made — you use the App Router, and this comparison does not apply. Where these three routers are the right question is React apps that own their routing and are not on a meta-framework: the classic case is a Vite single-page app, but also a dashboard shipped as a static bundle, an app embedded inside a larger page or a CMS, a browser extension, an Electron app, or any React UI that renders entirely on the client and needs URLs mapped to views. There is one interesting overlap worth knowing: TanStack Start is the meta-framework built on top of TanStack Router, so if you like TanStack Router's type-first, data-first approach and later want server-side rendering and a full-stack story, there is a natural upgrade path — the same way React Router v7 can grow from a SPA router into a full framework. But the rule stands: pick from React Router, TanStack Router, and Wouter only when you are the one who owns the router. If a framework already owns it, use what the framework gives you.

    Which router should a React template or SPA starter you sell ship with?

    For a template, SPA starter, or dashboard you intend to hand off or sell, the guidance mirrors every other infrastructure decision: default to what the buyer will recognize and can extend without archaeology, keep it type-safe and documented, and deviate only for a stated reason the buyer will understand. For a general-purpose React or Vite SPA template, React Router is usually the strongest default: it is the router almost every buyer already knows, so they can extend it without learning a new mental model, its documentation and community answers are everywhere when they get stuck, and v7 lets the same template start as a simple SPA and grow into a data-loading app without swapping the router — the lowest-friction path to a starter that feels familiar on clone. If the template's selling point is type safety and a data-heavy architecture — a serious admin dashboard, an analytics tool, a complex internal-tools starter — TanStack Router is the more impressive and more defensible default: typed routes and validated search params signal exactly the kind of production-ready rigor buyers pay for, and pairing it with TanStack Query makes the data story coherent, as long as you document the setup because it is less universally known. Wouter is the right default only for a deliberately lean template — a widget kit, a small embeddable app, a landing-page-plus-a-few-routes starter — where its 2 KB and zero config are part of the pitch. Whatever you choose, the resale rules are the same as any code you sell: keep routing definitions in one clear, documented place rather than scattered through components, type your params and (if the router supports it) your search params so a buyer cannot navigate to a route that does not exist, include a couple of nested routes and a not-found route so the buyer sees the pattern to copy, do not hardcode paths as magic strings everywhere, and write a short doc on how routes are structured and how to add one. A routing layer that is familiar, typed where possible, easy to extend, and documented does as much to make a codebase read as production-ready as the screens it navigates between.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →