diff --git a/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4 b/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4
new file mode 100644
index 00000000..9c543888
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph-2.mp4 differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4 b/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4
new file mode 100644
index 00000000..df593b12
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4 differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png
new file mode 100644
index 00000000..af9c4c31
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-react.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png
new file mode 100644
index 00000000..93f7f4f4
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-solid.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png
new file mode 100644
index 00000000..81857b21
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/bundle-size-history-vue.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png
new file mode 100644
index 00000000..4e45156f
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-react.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png
new file mode 100644
index 00000000..6e6c72a8
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-solid.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png
new file mode 100644
index 00000000..6992a459
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/client-side-nav-vue.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/header.jpg b/public/blog-assets/tanstack-router-signal-graph/header.jpg
new file mode 100644
index 00000000..9c03d0be
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/header.jpg differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png
new file mode 100644
index 00000000..8eebbb72
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-react.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png
new file mode 100644
index 00000000..8acaef18
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-solid.png differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png
new file mode 100644
index 00000000..ad03c507
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/store-updates-history-vue.png differ
diff --git a/src/blog/tanstack-router-signal-graph.md b/src/blog/tanstack-router-signal-graph.md
new file mode 100644
index 00000000..1ba84bbb
--- /dev/null
+++ b/src/blog/tanstack-router-signal-graph.md
@@ -0,0 +1,280 @@
+---
+published: 2026-03-15
+authors:
+ - Florian Pellet
+# title: 'How TanStack Router Became Granularly Reactive'
+# title: 'TanStack Router''s Granular Reactivity Rewrite'
+# title: 'Routing Is a Graph. Now Our Reactivity Is Too.'
+title: 'From One Big Router Store to a Granular Signal Graph'
+excerpt: TanStack Router now uses a granular signal graph as its reactive core. State is derived from that graph, narrowing change propagation and making client-side navigation substantially faster.
+---
+
+
+
+TanStack Router used to center most of its reactivity around one large object: `router.state`. [This refactor](https://github.com/TanStack/router/pull/6704) replaces that broad store with a graph of smaller stores. `router.state` is no longer the internal source of truth. It is now derived from the store graph.
+
+This builds on TanStack Store's migration[^alien-migration] to [alien-signals](https://github.com/stackblitz/alien-signals), implemented by [@DavidKPiano](https://github.com/davidkpiano). In external benchmarks[^alien-bench], alien-signals is one of the best-performing signals implementations tested. But the main improvement here is not just a faster primitive. It is a different reactive model.
+
+The result is
+
+- better update locality,
+- fewer store updates during navigation,
+- substantially faster client-side navigation,
+- the Solid adapter now uses native Solid signals internally.
+
+## Old Model: One Broad Router State
+
+The old model had one main reactive surface: `router.state`.
+
+That was useful. It made it possible to prototype features quickly and ship a broad API surface without first designing a perfect internal reactive topology. But it also meant many different concerns shared the same reactive entry point.
+
+| Concern | Stored under `router.state` | Typical consumer |
+| ----------------- | -------------------------------------------- | -------------------------------- |
+| Location | `location`, `resolvedLocation` | `useLocation`, `Link` |
+| Match lifecycle | `matches`, `pendingMatches`, `cachedMatches` | `useMatch`, `Matches`, `Outlet` |
+| Navigation status | `status`, `isLoading`, `isTransitioning` | pending UI, transitions |
+| Side effects | `redirect`, `statusCode` | navigation and response handling |
+
+This did not mean every update rerendered everything. Options like `select` and `structuralSharing` could prevent propagation. But many consumers still started from a broader subscription surface than they actually needed.
+
+## Problem: Routing State Has Locality
+
+Routing is not one thing that changes all at once. A navigation changes specific pieces of state with specific relationships: one match stays active, another becomes pending, one link flips state, some cached matches do not change at all.
+
+The old model captured those pieces of state, but it flattened them into one main subscription surface. This is where the mismatch becomes visible:
+
+
+
+
+A video showing that on every stateful event in the core of the router, changes are propagated to every subscription across the entire application.
+
+
+
+The point is that `router.state` was broader than what many consumers actually needed.
+
+## New Model: The Graph Becomes the Source of Truth
+
+The new model is not just "more stores". It inverts the relationship between `router.state` and the reactive graph.
+
+The broad surface is split into smaller stores with narrower responsibilities.
+
+- **top-level stores** for location, status, loading, transitions, redirects, and similar scalar state
+- **per-match stores** grouped into pools of active matches, pending matches, and cached matches.
+- **derived stores** for specific purposes like "is any match pending"
+
+`router.state` still exists for public APIs, but it is now recomputed from the store graph instead of serving as the internal source of truth.
+
+The new picture looks like this:
+
+
+
+
+A video showing that on each stateful event in the core of the router, only a specific subset of subscribers are updated in the application.
+
+
+
+> [!NOTE]
+> Active, pending, and cached matches are now modeled separately because
+> they have different lifecycles. This reduces state propagation even further.
+
+Before, the graph was derived from `router.state`. Now, `router.state` is derived from the graph. That inversion is the refactor.
+
+## Hook-Level Change: Subscribe to the Relevant Store
+
+Once the graph becomes the source of truth, router internals can subscribe directly to graph nodes instead of selecting from a broad snapshot. The clearest example is `useMatch`.
+
+Before this refactor, `useMatch` subscribed through the big router store and then searched `state.matches` for the match it cared about. Now it resolves the relevant store first and subscribes directly to it.
+
+```ts
+// Before
+useRouterState({
+ select: (state) => {
+ const match = state.matches.find((m) => m.routeId === routeId)
+ return /* select from one match */
+ }
+})
+
+// After
+const matchStore = router.stores.getMatchStoreByRouteId(routeId)
+useStore(matchStore, (match) => /* select from one match */)
+```
+
+This is an internal implementation detail, not a new public API surface for application code.
+
+> [!NOTE]
+> `getMatchStoreByRouteId` creates a derived signal on demand, and stores it
+> in a Least-Recently-Used cache[^lru-cache] so it can be reused by other subscribers
+> without leaking memory.
+
+The store-update-count graphs below show how many times subscriptions are invoked during various routing scenarios, before (curve is the entire history) and after (last point is this refactor).[^store-update-tests]
+
+
+
+#### React
+
+
+
+
+Absolute counts are not directly comparable across frameworks, because React, Solid, and Vue do not propagate updates in exactly the same way.
+
+
+
+#### Solid
+
+
+
+
+Absolute counts are not directly comparable across frameworks, because React, Solid, and Vue do not propagate updates in exactly the same way.
+
+
+
+#### Vue
+
+
+
+
+Absolute counts are not directly comparable across frameworks, because React, Solid, and Vue do not propagate updates in exactly the same way.
+
+
+
+
+
+These graphs show that change propagation got narrower.
+
+## Store Boundary: One Contract, Multiple Implementations
+
+The refactor did not only split router state into smaller stores. It also moved the store implementation behind a contract.
+
+The core now defines what a router store must do. Each adapter provides the implementation.
+
+```ts
+export interface RouterReadableStore {
+ readonly state: TValue
+}
+
+export interface RouterWritableStore {
+ readonly state: TValue
+ setState: (updater: (prev: TValue) => TValue) => void
+}
+
+export type StoreConfig = {
+ createMutableStore: MutableStoreFactory
+ createReadonlyStore: ReadonlyStoreFactory
+ batch: RouterBatchFn
+ init?: (stores: RouterStores) => void
+}
+```
+
+| Adapter | Store implementation |
+| :------ | :------------------- |
+| React | TanStack Store |
+| Vue | TanStack Store |
+| Solid | Solid signals |
+
+This keeps one router core while letting each adapter plug in the store model it wants.
+
+> [!NOTE]
+> Solid's derived stores are backed by native memos, and the adapter uses a `FinalizationRegistry`[^finalization-registry]
+> to dispose detached roots when those stores are garbage-collected.
+
+## Observable Result: Less Work During Navigation
+
+No new public API is required here. `useMatch`, `useLocation`, and `` keep the same surface. The difference is that navigation and preload flows now wake up fewer subscriptions.
+
+Our benchmarks isolate client-side navigation cost on a synthetic rerender-heavy page.[^client-nav-bench]
+
+- React: `7ms -> 4.5ms`
+- Solid: `12ms -> 8ms`
+- Vue: `7.5ms -> 6ms`
+
+
+
+#### React
+
+
+
+
+This graph shows the duration of 10 navigations on main (grey) and on refactor-signals (blue).
+
+
+
+#### Solid
+
+
+
+
+This graph shows the duration of 10 navigations on main (grey) and on refactor-signals (blue).
+
+
+
+#### Vue
+
+
+
+
+This graph shows the duration of 10 navigations on main (grey) and on refactor-signals (blue).
+
+
+
+
+
+There is also a bundle-size tradeoff. In our synthetic bundle-size benchmarks, measuring gzipped sizes:[^bundle-size-bench]
+
+- ↗ React increased by `~1KiB`
+- ↗ Vue increased by `~1KiB`
+- ↘ Solid decreased by `~1KiB`
+
+
+
+#### React
+
+
+
+
+Only relative changes matter in this benchmark, they are based on arbitrary apps and absolute sizes are not representative.
+
+
+
+#### Solid
+
+
+
+
+Only relative changes matter in this benchmark, they are based on arbitrary apps and absolute sizes are not representative.
+
+
+
+#### Vue
+
+
+
+
+Only relative changes matter in this benchmark, they are based on arbitrary apps and absolute sizes are not representative.
+
+
+
+
+
+## Closing
+
+This refactor did not just add signals to the old model. It inverted the reactivity model.
+
+Before, `router.state` was the broad reactive surface and the graph was derived from it. Now the graph is the primary model, and `router.state` is a compatibility snapshot derived from the graph.
+
+Routing is a graph. Now the reactivity is one too.
+
+---
+
+[^alien-migration]: [TanStack Store PR #265](https://github.com/TanStack/store/pull/265)
+
+[^alien-bench]: [js-reactivity-benchmark](https://github.com/transitive-bullshit/js-reactivity-benchmark) last updated january 2025
+
+[^store-update-tests]: Methodology and exact scenario assertions live in the adapter test files for [React](https://github.com/TanStack/router/blob/main/packages/react-router/tests/store-updates-during-navigation.test.tsx), [Solid](https://github.com/TanStack/router/blob/main/packages/solid-router/tests/store-updates-during-navigation.test.tsx), and [Vue](https://github.com/TanStack/router/blob/main/packages/vue-router/tests/store-updates-during-navigation.test.tsx).
+
+[^lru-cache]: For a great JavaScript-oriented explanation of how LRU caches work, see [Implementing an efficient LRU cache in JavaScript](https://yomguithereal.github.io/posts/lru-cache/).
+
+[^finalization-registry]: A [FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) allows us to hook into the Garbage Collector to execute arbitrary cleanup functions when an object gets collected.
+
+[^client-nav-bench]: These numbers come from the [`benchmarks/client-nav`](https://github.com/TanStack/router/tree/main/benchmarks/client-nav) CodSpeed suite, which runs a 10-navigation loop against a synthetic page that intentionally mounts many `useParams`, `useSearch`, and `Link` subscribers to amplify propagation costs. See [CodSpeed](https://codspeed.io/TanStack/router), and the [React app fixture](https://github.com/TanStack/router/blob/main/benchmarks/client-nav/react/app.tsx).
+
+[^bundle-size-bench]: These numbers come from the deterministic fixtures in [`benchmarks/bundle-size`](https://github.com/TanStack/router/tree/main/benchmarks/bundle-size), measured from the initial-load JS graph and tracked primarily as gzip deltas. See the [README](https://github.com/TanStack/router/blob/main/benchmarks/bundle-size/README.md).