diff --git a/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph.mp4 b/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph.mp4
new file mode 100644
index 00000000..6764956d
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/after-granular-store-graph.mp4 differ
diff --git a/public/blog-assets/tanstack-router-signal-graph/before-router-state-blob.mp4 b/public/blog-assets/tanstack-router-signal-graph/before-router-state-blob.mp4
new file mode 100644
index 00000000..45b4a203
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/before-router-state-blob.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.png b/public/blog-assets/tanstack-router-signal-graph/header.png
new file mode 100644
index 00000000..7924f7e8
Binary files /dev/null and b/public/blog-assets/tanstack-router-signal-graph/header.png 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..f1e288a9
--- /dev/null
+++ b/src/blog/tanstack-router-signal-graph.md
@@ -0,0 +1,262 @@
+---
+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 to [alien-signals](https://github.com/stackblitz/alien-signals) in [TanStack Store PR #265](https://github.com/TanStack/store/pull/265), implemented by [@DavidKPiano](https://github.com/davidkpiano). In external benchmarks like [js-reactivity-benchmark](https://github.com/transitive-bullshit/js-reactivity-benchmark), alien-signals is currently the best-performing signals implementation 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,
+- and Solid can now use its native signals.
+
+## 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 as a compatibility snapshot for public APIs. It is just no longer the primary model that everything else hangs off.
+
+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, hooks 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 */)
+```
+
+> [!NOTE]
+> `getMatchStoreByRouteId` creates a derived signal on demand, and stores it
+> in a Least-Recently-Used 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).
+
+
+
+#### 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 are the most direct proof 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`
+> 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.
+
+- 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:
+
+- ↗ 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.