---
title: "07 — Routing in React — URLs as State, React Router and TanStack Router"
uid: routing
tags: ["react-router", "react", "roadmap:react", "routing", "spa", "tanstack-router"]
excerpt: "In a single-page app the URL is just another piece of state, and a router maps URL state to which components render. Links are state setters; the back button is an undo."
date: 2026-08-13T03:27:45+0000
source: https://www.aveshina.my.id/en/blog/routing
---

"Just navigating between pages" was my routing model, and it made routers look like page-switching utilities. The framing that straightened it out: **in a single-page application, the URL is just another piece of state, and a router is the library that maps URL state to which components render.** [1] Once I saw the URL as state, the rest followed — links are state setters, the back button is an undo, and route params are state that flows into components.

## Why a single-page app needs a router at all

A traditional multi-page site navigates by full document reloads — each link fetches a fresh HTML page from the server. A React SPA loads once and then rewrites parts of the page in place, so "navigation" can't rely on the browser's default reload behavior. A client-side router fills that gap by:

- intercepting link clicks and pushState calls so they update the URL **without** a full reload,
- reading the current URL and deciding which route component to mount,
- syncing with the browser's back/forward buttons so history still works.

The payoff is the SPA's signature: instant page transitions, no white flash, persistent UI state across "pages." The cost is that I now own the URL-to-component mapping, which is what a router library manages for me.

## React Router: the default

React Router is the standard library for this [1][2]. It provides a declarative way to define routes — each route maps a URL pattern to a component — and handles the link interception, history, and nested routing. The core pieces:

- **<BrowserRouter>** wraps the app and enables client-side routing.
- **<Routes> and <Route>** declare the URL → component mapping, including dynamic segments like :id.
- **<Link>** renders an anchor that changes the URL without a reload.
- **useParams** reads dynamic segments; **useNavigate** pushes programmatically.

```
<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/users/:id" element={<UserProfile />} />
    <Route path="*" element={<NotFound />} />
  </Routes>
</BrowserRouter>
```

Routes nest, so a /dashboard/settings route can render <Dashboard> with an <Outlet> for its child routes. That nesting model is the part I underused at first — it's how shared layouts (a sidebar that stays put while the inner panel changes) are built.

## TanStack Router: the type-safe challenger

TanStack Router is a newer, more ambitious router [3]. Its distinguishing trait is **full type safety end to end** — route params, search/query parameters, and links are all typed, so a typo in a path or a wrong param type is a compile error rather than a runtime 404. It also treats search params as first-class typed state, with built-in validation.

```
// a route definition infers its params and search schema
export const Route = createFileRoute('/users/$userId')({
  validateSearch: (search) => ({ tab: search.tab ?? 'overview' }),
  component: UserProfile,
});
```

The tradeoff is a steeper setup — file-based routing convention, a build step, and a stricter way of thinking. The roadmap lists it alongside React Router [3], and the way I read it: TanStack Router is the choice when type safety around navigation and search params is worth the setup cost (large apps with complex query-param state), while React Router remains the sensible default for most projects.

```figure
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Routing maps URL state to components. A browser address bar /users/42 is parsed into segments; the router matches the /users/:id route and mounts UserProfile, passing id=42 as a param. A back arrow loops to history.">
  <defs>
    <marker id="tarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- address bar -->
    <rect x="60" y="20" width="420" height="36" rx="8" fill="#1f2937" stroke="#374151" stroke-width="1.5"/>
    <text x="80" y="43" font-size="12" font-family="ui-monospace,monospace" fill="#f9fafb">aveshina.my.id/users/42</text>

    <!-- parse arrow -->
    <path d="M270,56 L270,90" stroke="#64748b" stroke-width="1.5" marker-end="url(#tarrow)"/>
    <text x="280" y="78" font-size="10" fill="#64748b">router parses URL</text>

    <!-- route match -->
    <rect x="100" y="100" width="340" height="44" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="270" y="120" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">route table match</text>
    <text x="270" y="136" font-size="10.5" font-family="ui-monospace,monospace" fill="#4338ca" text-anchor="middle">/users/:id  →  &lt;UserProfile/&gt;</text>

    <!-- param extraction -->
    <path d="M270,144 L270,170" stroke="#64748b" stroke-width="1.5" marker-end="url(#tarrow)"/>
    <text x="380" y="160" font-size="10" fill="#64748b">id = 42</text>

    <!-- component mounts -->
    <rect x="160" y="180" width="220" height="44" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="270" y="208" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">&lt;UserProfile id={42}/&gt; mounts</text>

    <!-- history loop -->
    <path d="M540,38 C600,38 620,210 380,210" fill="none" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#tarrow)"/>
    <text x="610" y="120" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">back/forward</text>
    <text x="610" y="134" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">= undo/redo</text>

    <text x="370" y="270" font-size="11" fill="#475569" text-anchor="middle" font-style="italic">URL is state · router maps it to a component · history still works</text>
  </g>
</svg>
```

## How I use this

For most projects I default to React Router — it's battle-tested, the API is stable, and nested routes with <Outlet> cover the layout cases I hit. I treat the URL as state from day one: anything the user might want to share or bookmark (a filter, a selected tab, a detail view) goes into the URL as a path segment or search param, not just into local component state. That habit is what makes "deep links work" a side effect of how I build rather than a feature I bolt on. When a project's search-param state gets complex enough that I'm writing manual parsers and validators, that's the signal to evaluate TanStack Router — the type safety around params and search pays for itself at scale.

## References

[1] LogRocket, "How to use routing in React JS: a comprehensive guide," blog.logrocket.com, 2023. [Online]. Available: [https://blog.logrocket.com/react-router-v6-guide/](https://blog.logrocket.com/react-router-v6-guide/)

[2] React Router team, "React Router," 2024. [Online]. Available: [https://reactrouter.com/](https://reactrouter.com/)

[3] TanStack, "TanStack Router documentation," 2024. [Online]. Available: [https://tanstack.com/router/latest/docs/framework/react/overview](https://tanstack.com/router/latest/docs/framework/react/overview)

[4] pedrotech, "React Router v7: a crash course," dev.to, 2024. [Online]. Available: [https://dev.to/pedrotech/react-router-v7-a-crash-course-2m86](https://dev.to/pedrotech/react-router-v7-a-crash-course-2m86)

```quiz
Q: In a React SPA, what is the URL?
- a server-side concept with no client relevance
- a piece of state, and the router maps it to which components render
- an opaque string the browser owns entirely
correct: 1
explain: In a SPA, the URL is state. The router reads it and mounts the matching components; links and pushState update it without a full reload.

Q: React Router's nested routes plus <Outlet> let you…
- render shared layout (sidebar, header) that stays put while the inner panel changes per child route
- avoid using <Link> entirely
- bypass the browser history
correct: 0
explain: Nested routes map URL segments to nested components; an <Outlet> in the parent renders the matched child, so layout chrome persists across child-route changes.

Q: What is TanStack Router's headline advantage over React Router?
- it's faster at runtime by avoiding reconciliation
- end-to-end type safety for routes, params, and search params
- it doesn't need a build step
correct: 1
explain: TanStack Router treats params and search/query state as typed, validated values, so path typos and wrong param types surface as compile errors. It also has a stricter setup.

Q: Which hook reads a dynamic segment like :id from the current route in React Router?
- useParams
- useNavigate
- useState
correct: 0
explain: useParams returns an object with the dynamic segments of the matched route; useNavigate pushes programmatically.

Q: "Put it in the URL, not in component state" applies best to…
- values the user should be able to share, bookmark, or reach via the back button (filters, selected tabs, detail ids)
- transient form input like an unsubmitted text field
- a hover state
correct: 0
explain: Anything shareable/bookmarkable belongs in the URL as a path segment or search param. Transient UI state (hover, draft input) belongs in component state.
```
