Skip to content

Commit fa5b26c

Browse files
committed
cleanup most technical sentences to be more understandable
1 parent 6d45c75 commit fa5b26c

File tree

1 file changed

+30
-28
lines changed

1 file changed

+30
-28
lines changed

src/blog/tanstack-router-signal-graph.md

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ authors:
66
# title: 'TanStack Router''s Granular Reactivity Rewrite'
77
# title: 'Routing Is a Graph. Now Our Reactivity Is Too.'
88
title: 'From One Big Router Store to a Granular Signal Graph'
9-
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.
9+
excerpt: TanStack Router now uses a granular signal graph as its reactive core. State is derived from that graph, which narrows change propagation and makes client-side navigation faster in our benchmarks.
1010
---
1111

1212
![veins of emerald as a signal graph embedded in the rock of a tropical island](/blog-assets/tanstack-router-signal-graph/header.jpg)
1313

14-
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.
14+
TanStack Router used to keep all of its reactive state in one large object: `router.state`. [This refactor](https://github.com/TanStack/router/pull/6704) replaces that with a graph of smaller stores for the pieces of state that change independently. `router.state` still exists, but it is now derived from those stores instead of serving as the internal source of truth.
1515

16-
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.
16+
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 performed very well. The faster primitive helps, but the bigger change is that this allows the router to track state in smaller pieces instead of routing everything through one broad store.
1717

18-
The result is
18+
Concretely, this means:
1919

20-
- better update locality,
20+
- more targeted updates,
2121
- fewer store updates during navigation,
22-
- substantially faster client-side navigation,
22+
- faster client-side navigation in our benchmarks,
2323
- the Solid adapter now uses native Solid signals internally.
2424

2525
## Old Model: One Broad Router State
@@ -35,13 +35,13 @@ That was useful. It made it possible to prototype features quickly and ship a br
3535
| Navigation status | `status`, `isLoading`, `isTransitioning` | pending UI, transitions |
3636
| Side effects | `redirect`, `statusCode` | navigation and response handling |
3737

38-
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.
38+
This did not mean every update rerendered everything. Options like `select` and `structuralSharing` could prevent propagation. But many consumers still subscribed to more router state than they actually needed.
3939

40-
## Problem: Routing State Has Locality
40+
## Problem: Routing State Changes in Smaller Pieces
4141

42-
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.
42+
Routing state does not change as one unit. During a navigation, one match stays active, another becomes pending, one link changes state, and some cached matches do not change at all.
4343

44-
The old model captured those pieces of state, but it flattened them into one main subscription surface. This is where the mismatch becomes visible:
44+
The old model captured those pieces of state, but all subscriptions still started from the same top-level state object. That mismatch shows up here:
4545

4646
<figure>
4747
<video src="/blog-assets/tanstack-router-signal-graph/before-granular-store-graph-2.mp4" playsinline loop autoplay muted></video>
@@ -50,19 +50,19 @@ A video showing that on every stateful event in the core of the router, changes
5050
</figcaption>
5151
</figure>
5252

53-
The point is that `router.state` was broader than what many consumers actually needed.
53+
In practice, many consumers subscribed to more router state than they actually needed.
5454

55-
## New Model: The Graph Becomes the Source of Truth
55+
## New Model: Smaller Stores Become the Source of Truth
5656

57-
The new model is not just "more stores". It inverts the relationship between `router.state` and the reactive graph.
57+
The main change is that the smaller stores are now the source of truth, and `router.state` is rebuilt from them.
5858

59-
The broad surface is split into smaller stores with narrower responsibilities.
59+
Instead of one broad state object, the router keeps separate stores with narrower responsibilities.
6060

6161
- **top-level stores** for location, status, loading, transitions, redirects, and similar scalar state
6262
- **per-match stores** grouped into pools of active matches, pending matches, and cached matches.
6363
- **derived stores** for specific purposes like "is any match pending"
6464

65-
`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.
65+
`router.state` still exists for public APIs, but it is now rebuilt from the store graph instead of serving as the internal source of truth.
6666

6767
The new picture looks like this:
6868

@@ -75,13 +75,13 @@ A video showing that on each stateful event in the core of the router, only a sp
7575

7676
> [!NOTE]
7777
> Active, pending, and cached matches are now modeled separately because
78-
> they have different lifecycles. This reduces state propagation even further.
78+
> they have different lifecycles. This cuts down updates even further.
7979
80-
Before, the graph was derived from `router.state`. Now, `router.state` is derived from the graph. That inversion is the refactor.
80+
Before, the smaller pieces of state were derived from `router.state`. Now, `router.state` is derived from the smaller stores. That is the core of this refactor.
8181

8282
## Hook-Level Change: Subscribe to the Relevant Store
8383

84-
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`.
84+
With the smaller stores as the source of truth, router internals can subscribe to the exact store they need instead of selecting from one large snapshot. The clearest example is `useMatch`.
8585

8686
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.
8787

@@ -102,11 +102,11 @@ useStore(matchStore, (match) => /* select from one match */)
102102
This is an internal implementation detail, not a new public API surface for application code.
103103

104104
> [!NOTE]
105-
> `getMatchStoreByRouteId` creates a derived signal on demand, and stores it
106-
> in a Least-Recently-Used cache[^lru-cache] so it can be reused by other subscribers
105+
> `getMatchStoreByRouteId` creates the derived signal on demand and stores it
106+
> in a Least-Recently-Used cache[^lru-cache] so other subscribers can reuse it
107107
> without leaking memory.
108108
109-
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]
109+
The store-update-count graphs below show how many times subscriptions are invoked during various routing scenarios. The last point is this refactor.[^store-update-tests]
110110

111111
<!-- ::start:tabs -->
112112

@@ -139,13 +139,13 @@ Absolute counts are not directly comparable across frameworks, because React, So
139139

140140
<!-- ::end:tabs -->
141141

142-
These graphs show that change propagation got narrower.
142+
These graphs show that fewer subscribers are triggered during navigation.
143143

144144
## Store Boundary: One Contract, Multiple Implementations
145145

146-
The refactor did not only split router state into smaller stores. It also moved the store implementation behind a contract.
146+
The refactor also moves the store implementation behind a shared contract.
147147

148-
The core now defines what a router store must do. Each adapter provides the implementation.
148+
The router core defines the interface. Each adapter provides the implementation.
149149

150150
```ts
151151
export interface RouterReadableStore<TValue> {
@@ -179,7 +179,7 @@ This keeps one router core while letting each adapter plug in the store model it
179179
180180
## Observable Result: Less Work During Navigation
181181
182-
No new public API is required here. `useMatch`, `useLocation`, and `<Link>` keep the same surface. The difference is that navigation and preload flows now wake up fewer subscriptions.
182+
No new public API is required here. `useMatch`, `useLocation`, and `<Link>` keep the same surface. The difference is that navigation and preload flows now trigger fewer subscriptions.
183183
184184
Our benchmarks isolate client-side navigation cost on a synthetic rerender-heavy page.[^client-nav-bench]
185185
@@ -224,6 +224,8 @@ There is also a bundle-size tradeoff. In our synthetic bundle-size benchmarks, m
224224
- ↗ Vue increased by `~1KiB`
225225
- ↘ Solid decreased by `~1KiB`
226226
227+
React and Vue increased in size because representing the router as several stores takes more code than representing it as one state object. Solid decreased in size because it no longer depends on `tanstack/store`.
228+
227229
<!-- ::start:tabs -->
228230
229231
#### React
@@ -257,11 +259,11 @@ Only relative changes matter in this benchmark, they are based on arbitrary apps
257259
258260
## Closing
259261
260-
This refactor did not just add signals to the old model. It inverted the reactivity model.
262+
This refactor changes how reactivity is structured inside the router.
261263
262-
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.
264+
Before, `router.state` was the broad reactive surface and smaller pieces of state were derived from it. Now the smaller stores are primary, and `router.state` is a derived snapshot kept for the existing public API.
263265
264-
Routing is a graph. Now the reactivity is one too.
266+
In practice, that means route changes update more locally and trigger less work during navigation.
265267
266268
---
267269

0 commit comments

Comments
 (0)