惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
博客园 - 司徒正美
A
About on SuperTechFans
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
爱范儿
爱范儿
I
InfoQ
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
F
Fortinet All Blogs
S
SegmentFault 最新的问题
阮一峰的网络日志
阮一峰的网络日志
D
Docker
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
M
MIT News - Artificial intelligence
Jina AI
Jina AI
H
Help Net Security
量子位
IT之家
IT之家

TanStack Blog

TanStack + Vercel Partnership | TanStack Blog TanStack AI Enters the RC Phase | TanStack Blog Inside a TanStack Router Navigation | TanStack Blog Form v2 is here: All you need to know about the alpha | TanStack Blog Announcing TanStack Table V9 | TanStack Blog TanStack Has a New Look | TanStack Blog Introducing TanStack Markdown and TanStack Highlight | TanStack Blog We Removed React Server Components from TanStack.com | TanStack Blog We Stopped Using RSC on TanStack.com | TanStack Blog Inside TanStack Table V9 Reactivity | TanStack Blog Run Any Coding Agent in a Sandbox, With One chat() Call | TanStack Blog TanStack Start and TanStack AI Win 2026 Open Source Awards | TanStack Blog How an Underrated Refactor Saved 90% Memory Usage | TanStack Blog TypeScript Performance in TanStack Table V9 | TanStack Blog TanStack AI Beta: The Switzerland of AI Tooling Grows Up | TanStack Blog TanStack Table V9: Taking Form | TanStack Blog TanStack AI: Your MCP, your way | TanStack Blog TanStack Start Adds First-Class Rsbuild Support | TanStack Blog Introducing Experimental Workflows and Orchestrators in TanStack AI | TanStack Blog Chat UIs Are Lists Until They Aren't | TanStack Blog Structured Output That Remembers Across Turns | TanStack Blog TanStack Virtual just got a lot faster, and finally handles iOS | TanStack Blog TanStack AI now fully speaks AG-UI | TanStack Blog Stop Waiting on JSON: Stream Structured Output with One Schema | TanStack Blog Hardening TanStack After the npm Compromise | TanStack Blog Postmortem: TanStack npm supply-chain compromise | TanStack Blog Who Owns the Tree? RSC as a Protocol, Not an Architecture | TanStack Blog TanStack AI Just Learned to Compose Music | TanStack Blog Your AI Tool Calls Should Fail at Compile Time, Not in Production | TanStack Blog One Flag, Every Chunk: Debug Logging Lands in TanStack AI | TanStack Blog
TanStack Router's New Reactive Core: A Signal Graph | Tan...
Florian Pellet · 2026-03-31 · via TanStack Blog

by Florian Pellet on Mar 31, 2026.

veins of emerald as a signal graph embedded in the rock of a tropical island

TanStack Router used to keep all of its reactive state in one large object: router.state. This refactor 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.

This builds on TanStack Store's migration1 to alien-signals, implemented by @DavidKPiano. In external benchmarks2, 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.

Concretely, this means:

  • more targeted updates,
  • fewer store updates during navigation,
  • faster client-side navigation in our benchmarks,
  • the Solid adapter now uses native Solid signals internally.

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.

ConcernStored under router.stateTypical consumer
Locationlocation, resolvedLocationuseLocation, Link
Match lifecyclematches, pendingMatches, cachedMatchesuseMatch, Matches, Outlet
Navigation statusstatus, isLoading, isTransitioningpending UI, transitions
Side effectsredirect, statusCodenavigation and response handling

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.

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.

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:

A video showing that on every stateful event in the core of the router, changes are propagated to every subscription across the entire application.

In practice, many consumers subscribed to more router state than they actually needed.

The main change is that the smaller stores are now the source of truth, and router.state is rebuilt from them.

Instead of one broad state object, the router keeps separate 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 rebuilt 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.

Active, pending, and cached matches are now modeled separately because they have different lifecycles. This cuts down updates even further.

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.

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.

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.

This is an internal implementation detail, not a new public API surface for application code.

getMatchStoreByRouteId creates the derived signal on demand and stores it in a Least-Recently-Used cache3 so other subscribers can reuse it without leaking memory.

The store-update-count graphs below show how many times subscriptions are invoked during various routing scenarios. The last point is this refactor.4

These graphs show that fewer subscribers are triggered during navigation.

Vue Router is mentioned throughout this article as a useful reference. However it is still a work in progress. Vue Vapor (3.6) is on the doorstep (beta.9 at the time of writing), so the plan is to do the Vapor refactor and then support that refreshed version.

The refactor also moves the store implementation behind a shared contract.

The router core defines the interface. Each adapter provides the implementation.

AdapterStore implementation
ReactTanStack Store
VueTanStack Store
SolidSolid signals

This keeps one router core while letting each adapter plug in the store model it wants.

Solid's derived stores are backed by native memos, and the adapter uses a FinalizationRegistry5 to dispose detached roots when those stores are garbage-collected.

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.

Our benchmarks isolate client-side navigation cost on a synthetic rerender-heavy page.6

  • React: 7ms -> 4.5ms
  • Solid: 12ms -> 8ms
  • Vue: 7.5ms -> 6ms

There is also a bundle-size tradeoff. In our synthetic bundle-size benchmarks, measuring gzipped sizes:7

  • ↗ React increased by ~1KiB
  • ↗ Vue increased by ~1KiB
  • ↘ Solid decreased by ~1KiB

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.

This refactor changes how reactivity is structured inside the router.

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.

In practice, that means route changes update more locally and trigger less work during navigation.


  1. TanStack Store PR #265

  2. js-reactivity-benchmark last updated January 2025 ↩

  3. For a great JavaScript-oriented explanation of how LRU caches work, see Implementing an efficient LRU cache in JavaScript. ↩

  4. Methodology and exact scenario assertions live in the adapter test files for React, Solid, and Vue. ↩

  5. A FinalizationRegistry allows us to hook into the Garbage Collector to execute arbitrary cleanup functions when an object gets collected. ↩

  6. These numbers come from the 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, and the React app fixture. ↩

  7. These numbers come from the deterministic fixtures in benchmarks/bundle-size, measured from the initial-load JS graph and tracked primarily as gzip deltas. See the README. ↩