tanstack-router-cache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +84 -0
- package/dist/components/cached-outlet.d.ts +8 -0
- package/dist/components/off-screen-in.d.ts +9 -0
- package/dist/components/off-screen.d.ts +4 -0
- package/dist/components/restore-cached-href.d.ts +10 -0
- package/dist/components/route-cache-manager.d.ts +2 -0
- package/dist/components/router-cache-outlet.d.ts +4 -0
- package/dist/contexts/router-cache.d.ts +36 -0
- package/dist/dom/dismiss-transient-ui.d.ts +3 -0
- package/dist/hooks/use-event-listener.d.ts +36 -0
- package/dist/hooks/use-route-cache-active.d.ts +1 -0
- package/dist/hooks/use-route-cache-activity.d.ts +1 -0
- package/dist/hooks/use-route-cache-effect.d.ts +2 -0
- package/dist/hooks/use-route-cache-error-boundary.d.ts +1 -0
- package/dist/hooks/use-route-cache-navigation.d.ts +7 -0
- package/dist/hooks/use-router-cache-debug.d.ts +24 -0
- package/dist/hooks/use-router-cache.d.ts +10 -0
- package/dist/hooks/use-update.d.ts +1 -0
- package/dist/index.cjs +66 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +10 -0
- package/dist/pathname.d.ts +1 -0
- package/dist/types.d.ts +10 -0
- package/docs/architecture.md +503 -0
- package/docs/cache-behavior.md +28 -0
- package/docs/components.md +41 -0
- package/docs/debugging.md +31 -0
- package/docs/getting-started.md +151 -0
- package/docs/hooks.md +187 -0
- package/docs/types.md +37 -0
- package/docs/usage.md +11 -0
- package/package.json +63 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This page explains how `tanstack-router-cache` is built: runtime pieces, state shape, rendering flow, eviction, events, and memory behavior.
|
|
4
|
+
|
|
5
|
+
## Quick map
|
|
6
|
+
|
|
7
|
+
`tanstack-router-cache` replaces the normal TanStack Router outlet area with a cache manager. The live route still comes from TanStack Router. Cacheable route trees are captured after they become ready, then rendered from a stored router snapshot when they should stay mounted.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TD
|
|
11
|
+
router["TanStack Router state"]
|
|
12
|
+
provider["RouterCacheProvider"]
|
|
13
|
+
outlet["RouterCacheOutlet"]
|
|
14
|
+
manager["Cache manager"]
|
|
15
|
+
live["Live Outlet"]
|
|
16
|
+
cache["CachedRoutes map"]
|
|
17
|
+
cachedOutlet["CachedOutlet"]
|
|
18
|
+
snapshot["Router snapshot"]
|
|
19
|
+
activity["React Activity container"]
|
|
20
|
+
hooks["Hooks and debug API"]
|
|
21
|
+
|
|
22
|
+
provider --> outlet
|
|
23
|
+
router --> manager
|
|
24
|
+
outlet --> manager
|
|
25
|
+
manager --> live
|
|
26
|
+
manager --> cache
|
|
27
|
+
cache --> cachedOutlet
|
|
28
|
+
cachedOutlet --> snapshot
|
|
29
|
+
snapshot --> activity
|
|
30
|
+
manager --> hooks
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Runtime pieces
|
|
34
|
+
|
|
35
|
+
| Piece | Responsibility |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `RouterCacheProvider` | Owns cache state, cache limits, scope resets, and errored-route tracking. |
|
|
38
|
+
| `RouterCacheOutlet` | Replaces TanStack Router's outlet for the route branch that can be cached. |
|
|
39
|
+
| Cache manager | Reads live router state, decides whether to render the live outlet or cached outlets, and synchronizes cache entries. |
|
|
40
|
+
| `CachedOutlet` | Renders a cached route from a stored router snapshot and match id. |
|
|
41
|
+
| `OffScreen` | Wraps cached route content in React `Activity` and marks the route as `visible` or `hidden`. |
|
|
42
|
+
| Event listener | Emits route activity and cached-navigation lifecycle events used by hooks. |
|
|
43
|
+
| Debug hook | Exposes development diagnostics on `window.__TANSTACK_ROUTER_CACHE_DEBUG__`. |
|
|
44
|
+
| Transient UI tracker | Tracks external UI added outside the route container and hides or restores it with the owning route. |
|
|
45
|
+
|
|
46
|
+
## Route lifecycle
|
|
47
|
+
|
|
48
|
+
A route becomes cacheable only after the current match is resolved, successful, and marked with `staticData.routeCache: true`.
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
stateDiagram-v2
|
|
52
|
+
[*] --> LiveRoute: normal TanStack render
|
|
53
|
+
LiveRoute --> WaitingForReady: routeCache true
|
|
54
|
+
WaitingForReady --> CachedReady: match success and snapshot stored
|
|
55
|
+
CachedReady --> VisibleCached: pathname is visible
|
|
56
|
+
VisibleCached --> HiddenCached: navigate away
|
|
57
|
+
HiddenCached --> VisibleCached: navigate back
|
|
58
|
+
HiddenCached --> Evicted: cache limit or manual invalidation
|
|
59
|
+
VisibleCached --> Evicted: route error or no longer cacheable
|
|
60
|
+
Evicted --> [*]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In normal usage, a cached route alternates between `visible` and `hidden`. It leaves the cache when a limit evicts it, the app invalidates it, the route stops being cacheable, or an error boundary marks it as failed.
|
|
64
|
+
|
|
65
|
+
## Provider state
|
|
66
|
+
|
|
67
|
+
The provider stores cached routes by normalized pathname. The same object is exposed publicly as `cachedRoutes`.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type CachedRoutes = {
|
|
71
|
+
[normalizedPathname: string]: CachedRouteData;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type ErroredRouteCounts = Record<string, number>;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
flowchart LR
|
|
79
|
+
provider["RouterCacheProvider"]
|
|
80
|
+
routes["CachedRoutes map"]
|
|
81
|
+
errors["ErroredRouteCounts"]
|
|
82
|
+
limits["Cache limits"]
|
|
83
|
+
scope["cacheScopeKey"]
|
|
84
|
+
|
|
85
|
+
provider --> routes
|
|
86
|
+
provider --> errors
|
|
87
|
+
provider --> limits
|
|
88
|
+
scope --> provider
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`ErroredRouteCounts` prevents failed cached views from being reused while an error fallback is mounted. It is count-based so repeated error-boundary hooks can retain and release the same pathname safely.
|
|
92
|
+
|
|
93
|
+
The provider exposes these operations to the cache manager and hooks:
|
|
94
|
+
|
|
95
|
+
| Operation | Purpose |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| Upsert cached route | Insert or update one cached route entry after normalizing the pathname. |
|
|
98
|
+
| Delete cached routes | Remove cached entries. Used by invalidation, errored routes, and cache manager cleanup. |
|
|
99
|
+
| Touch cached routes | Update `lastVisibleAt` when a cached route becomes visible. |
|
|
100
|
+
| Retain errored route | Mark a pathname as currently errored and remove its cached entry. |
|
|
101
|
+
| Release errored route | Release one error retain count for a pathname. |
|
|
102
|
+
|
|
103
|
+
## Cached route data
|
|
104
|
+
|
|
105
|
+
Each cache entry is small. The large memory cost is the retained React tree, not this data object.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type CachedRouteData = {
|
|
109
|
+
createdAt?: number;
|
|
110
|
+
href?: string;
|
|
111
|
+
lastVisibleAt?: number;
|
|
112
|
+
routeId?: string;
|
|
113
|
+
staticData: StaticDataRouteOption;
|
|
114
|
+
matchId?: string;
|
|
115
|
+
routerSnapshot?: RouterSnapshot;
|
|
116
|
+
ready?: boolean;
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```mermaid
|
|
121
|
+
classDiagram
|
|
122
|
+
class CachedRouteData {
|
|
123
|
+
createdAt
|
|
124
|
+
href
|
|
125
|
+
lastVisibleAt
|
|
126
|
+
routeId
|
|
127
|
+
staticData
|
|
128
|
+
matchId
|
|
129
|
+
routerSnapshot
|
|
130
|
+
ready
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class RouterSnapshot {
|
|
134
|
+
static stores
|
|
135
|
+
static matches
|
|
136
|
+
live navigate
|
|
137
|
+
live invalidate
|
|
138
|
+
live preloadRoute
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
CachedRouteData --> RouterSnapshot : uses
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Field | Role |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `createdAt` | First time the entry was stored. Used as an eviction fallback. |
|
|
147
|
+
| `href` | Full route href, including search and hash when available. Used for restoration. |
|
|
148
|
+
| `lastVisibleAt` | Last time the entry became visible. Primary eviction timestamp. |
|
|
149
|
+
| `routeId` | TanStack route id. Used by `maxEntriesPerRouteId`. |
|
|
150
|
+
| `staticData` | Route static data. The route is cacheable when `routeCache` is `true`. |
|
|
151
|
+
| `matchId` | Match id used to render the cached route with TanStack Router's `Match`. |
|
|
152
|
+
| `routerSnapshot` | Frozen router-like object used by the cached route tree. |
|
|
153
|
+
| `ready` | Marks that the route has a complete snapshot and can be rendered from cache. |
|
|
154
|
+
|
|
155
|
+
Pathnames are normalized by removing trailing slashes except for `/`, so `/customers/` and `/customers` share one cache key.
|
|
156
|
+
|
|
157
|
+
## Router snapshot
|
|
158
|
+
|
|
159
|
+
Cached route trees still expect TanStack Router context. Instead of keeping every cached tree connected to the live router stores, the package creates a router snapshot when a route becomes ready.
|
|
160
|
+
|
|
161
|
+
```mermaid
|
|
162
|
+
flowchart TD
|
|
163
|
+
liveRouter["Live router"]
|
|
164
|
+
matches["Current matches"]
|
|
165
|
+
location["Current location"]
|
|
166
|
+
staticStores["Static stores"]
|
|
167
|
+
liveMethods["Bound live methods"]
|
|
168
|
+
routerSnapshot["Router snapshot"]
|
|
169
|
+
cachedOutlet["CachedOutlet"]
|
|
170
|
+
match["Match by cached matchId"]
|
|
171
|
+
|
|
172
|
+
liveRouter --> matches
|
|
173
|
+
liveRouter --> location
|
|
174
|
+
matches --> staticStores
|
|
175
|
+
location --> staticStores
|
|
176
|
+
liveRouter --> liveMethods
|
|
177
|
+
staticStores --> routerSnapshot
|
|
178
|
+
liveMethods --> routerSnapshot
|
|
179
|
+
routerSnapshot --> cachedOutlet
|
|
180
|
+
cachedOutlet --> match
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The snapshot copies the current matches, location, resolved location, and match stores. It also keeps selected live router methods bound to the real router, including `navigate`, `invalidate`, `preloadRoute`, and location builders.
|
|
184
|
+
|
|
185
|
+
This gives hidden cached routes a stable route view while still allowing imperative router actions to call through to the real router.
|
|
186
|
+
|
|
187
|
+
`CachedOutlet` renders that snapshot like this:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<RouterContextProvider router={routerSnapshot}>
|
|
191
|
+
<Match matchId={matchId} />
|
|
192
|
+
</RouterContextProvider>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Cache synchronization
|
|
196
|
+
|
|
197
|
+
On every relevant router state change, the cache manager checks the current route state and updates the cache.
|
|
198
|
+
|
|
199
|
+
```mermaid
|
|
200
|
+
flowchart TD
|
|
201
|
+
start["Router state changed"]
|
|
202
|
+
read["Read pathname, href, matches, status"]
|
|
203
|
+
normalize["Normalize pathnames"]
|
|
204
|
+
staticData["Find deepest routeCache static data"]
|
|
205
|
+
errored{"Current route errored?"}
|
|
206
|
+
resolved{"Match resolved?"}
|
|
207
|
+
cacheable{"routeCache true?"}
|
|
208
|
+
ready{"Match successful?"}
|
|
209
|
+
write["Create or refresh cache entry"]
|
|
210
|
+
deleteEntry["Delete cache entry"]
|
|
211
|
+
wait["Wait for next router update"]
|
|
212
|
+
render["Render cached outlets and maybe live outlet"]
|
|
213
|
+
|
|
214
|
+
start --> read --> normalize --> staticData --> errored
|
|
215
|
+
errored -- yes --> deleteEntry --> render
|
|
216
|
+
errored -- no --> resolved
|
|
217
|
+
resolved -- no --> wait
|
|
218
|
+
resolved -- yes --> cacheable
|
|
219
|
+
cacheable -- no --> deleteEntry
|
|
220
|
+
cacheable -- yes --> ready
|
|
221
|
+
ready -- no --> wait
|
|
222
|
+
ready -- yes --> write --> render
|
|
223
|
+
deleteEntry --> render
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The current entry being written is protected during limit enforcement so the route that just became ready is not immediately evicted.
|
|
227
|
+
|
|
228
|
+
## Visible pathname
|
|
229
|
+
|
|
230
|
+
The manager tracks three pathnames:
|
|
231
|
+
|
|
232
|
+
| Name | Meaning |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `routerPathname` | Current router location pathname. |
|
|
235
|
+
| `resolvedPathname` | Resolved router location pathname. |
|
|
236
|
+
| `visiblePathname` | Cached pathname that should currently be shown. |
|
|
237
|
+
|
|
238
|
+
```mermaid
|
|
239
|
+
flowchart TD
|
|
240
|
+
routerPathname["routerPathname"]
|
|
241
|
+
resolvedPathname["resolvedPathname"]
|
|
242
|
+
readyDestination{"Destination is ready cached route?"}
|
|
243
|
+
ancestor{"Router pathname is ancestor?"}
|
|
244
|
+
useRouter["visiblePathname = routerPathname"]
|
|
245
|
+
useResolved["visiblePathname = resolvedPathname"]
|
|
246
|
+
|
|
247
|
+
routerPathname --> readyDestination
|
|
248
|
+
resolvedPathname --> readyDestination
|
|
249
|
+
readyDestination -- yes --> useRouter
|
|
250
|
+
readyDestination -- no --> ancestor
|
|
251
|
+
ancestor -- yes --> useRouter
|
|
252
|
+
ancestor -- no --> useResolved
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Most of the time, `visiblePathname` is the resolved pathname. During some navigations, TanStack Router can temporarily expose a router pathname that differs from the resolved pathname. If the destination is already cached, the package can show it immediately.
|
|
256
|
+
|
|
257
|
+
## Rendering model
|
|
258
|
+
|
|
259
|
+
Every ready cached entry is rendered through `OffScreen`.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<OffScreen mode={visiblePathname === pathname ? "visible" : "hidden"}>
|
|
263
|
+
<CachedOutlet matchId={route.matchId} routerSnapshot={route.routerSnapshot} />
|
|
264
|
+
</OffScreen>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```mermaid
|
|
268
|
+
flowchart TD
|
|
269
|
+
routes["CachedRoutes entries"]
|
|
270
|
+
entry["Ready cache entry"]
|
|
271
|
+
offscreen["OffScreen"]
|
|
272
|
+
activity["React Activity"]
|
|
273
|
+
mode{"Visible pathname matches entry?"}
|
|
274
|
+
visible["mode = visible"]
|
|
275
|
+
hidden["mode = hidden"]
|
|
276
|
+
dom["data-router-cache attributes"]
|
|
277
|
+
|
|
278
|
+
routes --> entry --> offscreen --> activity --> mode
|
|
279
|
+
mode -- yes --> visible --> dom
|
|
280
|
+
mode -- no --> hidden --> dom
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`OffScreen` uses React `Activity` with either `visible` or `hidden` mode. The route tree remains mounted in both modes. The wrapping element gets these attributes:
|
|
284
|
+
|
|
285
|
+
```html
|
|
286
|
+
<div
|
|
287
|
+
data-router-cache-container="true"
|
|
288
|
+
data-router-cache-mode="hidden"
|
|
289
|
+
data-router-cache-pathname="/customers"
|
|
290
|
+
>
|
|
291
|
+
...
|
|
292
|
+
</div>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Those attributes are used by diagnostics and transient UI tracking.
|
|
296
|
+
|
|
297
|
+
## Scroll and transient UI
|
|
298
|
+
|
|
299
|
+
The hidden route container is not enough for every UI primitive. Popovers, menus, dialogs, tooltips, and command palettes often render into portals outside the route container.
|
|
300
|
+
|
|
301
|
+
```mermaid
|
|
302
|
+
sequenceDiagram
|
|
303
|
+
participant Route as Visible route
|
|
304
|
+
participant DOM as Document
|
|
305
|
+
participant Tracker as Transient UI tracker
|
|
306
|
+
participant Portal as External portal element
|
|
307
|
+
|
|
308
|
+
Route->>DOM: Opens menu, dialog, tooltip, or popover
|
|
309
|
+
DOM->>Tracker: MutationObserver sees added element
|
|
310
|
+
Tracker->>Tracker: Assign owner pathname
|
|
311
|
+
Route->>Tracker: Route becomes hidden
|
|
312
|
+
Tracker->>Portal: Dispatch hover exit and Escape
|
|
313
|
+
Tracker->>Portal: Set display none, aria-hidden, inert
|
|
314
|
+
Route->>Tracker: Route becomes visible
|
|
315
|
+
Tracker->>Portal: Restore previous DOM state
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
External elements can opt out of this ownership behavior with:
|
|
319
|
+
|
|
320
|
+
```html
|
|
321
|
+
<div data-router-cache-persistent-external="true">
|
|
322
|
+
...
|
|
323
|
+
</div>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The package also stores window scroll positions by pathname. When a cached route becomes visible, it restores the saved window scroll position after two animation frames.
|
|
327
|
+
|
|
328
|
+
```mermaid
|
|
329
|
+
sequenceDiagram
|
|
330
|
+
participant Route as Cached route
|
|
331
|
+
participant Store as Scroll position map
|
|
332
|
+
participant Window as window
|
|
333
|
+
|
|
334
|
+
Route->>Store: Save scroll when hidden
|
|
335
|
+
Route->>Window: Become visible
|
|
336
|
+
Window-->>Route: Animation frame
|
|
337
|
+
Window-->>Route: Animation frame
|
|
338
|
+
Route->>Window: scrollTo saved x and y
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Eviction
|
|
342
|
+
|
|
343
|
+
The provider accepts two limits:
|
|
344
|
+
|
|
345
|
+
| Limit | Meaning |
|
|
346
|
+
| --- | --- |
|
|
347
|
+
| `maxEntries` | Maximum cached entries in the provider. |
|
|
348
|
+
| `maxEntriesPerRouteId` | Maximum cached entries for the same TanStack route id. |
|
|
349
|
+
|
|
350
|
+
Invalid, missing, `NaN`, and non-finite limits are normalized to `Infinity`. Negative values are normalized to `0`. `maxEntries={0}` disables caching and clears existing entries.
|
|
351
|
+
|
|
352
|
+
```mermaid
|
|
353
|
+
flowchart TD
|
|
354
|
+
input["Next CachedRoutes map"]
|
|
355
|
+
disabled{"maxEntries is 0?"}
|
|
356
|
+
protect["Protect current pathname"]
|
|
357
|
+
perRoute["Apply maxEntriesPerRouteId"]
|
|
358
|
+
global["Apply maxEntries"]
|
|
359
|
+
sort["Sort evictable entries by lastVisibleAt, createdAt, pathname"]
|
|
360
|
+
delete["Delete selected entries"]
|
|
361
|
+
output["Bounded CachedRoutes map"]
|
|
362
|
+
|
|
363
|
+
input --> disabled
|
|
364
|
+
disabled -- yes --> delete
|
|
365
|
+
disabled -- no --> protect --> perRoute --> global --> sort --> delete --> output
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Limit enforcement runs in this order:
|
|
369
|
+
|
|
370
|
+
1. Apply `maxEntriesPerRouteId`.
|
|
371
|
+
2. Apply `maxEntries`.
|
|
372
|
+
3. Keep protected keys, usually the route currently being written.
|
|
373
|
+
4. Evict least recently visible entries first.
|
|
374
|
+
5. If timestamps tie, sort by pathname for deterministic eviction.
|
|
375
|
+
|
|
376
|
+
The timestamp used for eviction is `lastVisibleAt`, then `createdAt`, then `0`.
|
|
377
|
+
|
|
378
|
+
## Href restoration
|
|
379
|
+
|
|
380
|
+
The cache key is pathname-based, but a route can be cached with a fuller href that includes search params or a hash.
|
|
381
|
+
|
|
382
|
+
```mermaid
|
|
383
|
+
flowchart TD
|
|
384
|
+
enter["Navigate to cached pathname"]
|
|
385
|
+
cachedHref{"Cached href exists?"}
|
|
386
|
+
bareHref{"Current href is bare pathname?"}
|
|
387
|
+
changed{"Cached href differs?"}
|
|
388
|
+
restore["navigate with replace true and resetScroll false"]
|
|
389
|
+
skip["Do nothing"]
|
|
390
|
+
|
|
391
|
+
enter --> cachedHref
|
|
392
|
+
cachedHref -- no --> skip
|
|
393
|
+
cachedHref -- yes --> bareHref
|
|
394
|
+
bareHref -- no --> skip
|
|
395
|
+
bareHref -- yes --> changed
|
|
396
|
+
changed -- no --> skip
|
|
397
|
+
changed -- yes --> restore
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
When restoration is needed, the package navigates back to the cached href:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
router.navigate({
|
|
404
|
+
href: cachedHref,
|
|
405
|
+
replace: true,
|
|
406
|
+
resetScroll: false,
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
This keeps cached list filters, tabs, anchors, or search state aligned with the retained route view.
|
|
411
|
+
|
|
412
|
+
## Events and hooks
|
|
413
|
+
|
|
414
|
+
The package uses a singleton internal event bus.
|
|
415
|
+
|
|
416
|
+
```mermaid
|
|
417
|
+
flowchart LR
|
|
418
|
+
manager["Cache manager"]
|
|
419
|
+
bus["Event bus"]
|
|
420
|
+
active["activeChange"]
|
|
421
|
+
navStart["cachedNavigationStart"]
|
|
422
|
+
navComplete["cachedNavigationComplete"]
|
|
423
|
+
navCancel["cachedNavigationCancel"]
|
|
424
|
+
hooks["Public hooks"]
|
|
425
|
+
|
|
426
|
+
manager --> bus
|
|
427
|
+
bus --> active
|
|
428
|
+
bus --> navStart
|
|
429
|
+
bus --> navComplete
|
|
430
|
+
bus --> navCancel
|
|
431
|
+
active --> hooks
|
|
432
|
+
navStart --> hooks
|
|
433
|
+
navComplete --> hooks
|
|
434
|
+
navCancel --> hooks
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
| Event | Payload | Used by |
|
|
438
|
+
| --- | --- | --- |
|
|
439
|
+
| `activeChange` | `{ pathname, mode }` | `useRouteCacheActive`, `useRouteCacheEffect`, `useRouteCacheActivity`, `useRouterCache` refreshes. |
|
|
440
|
+
| `cachedNavigationStart` | `{ pathname, startedAt }` | `useRouteCacheNavigation`. |
|
|
441
|
+
| `cachedNavigationComplete` | `{ pathname, startedAt, visibleAt, paintedAt, duration }` | `useRouteCacheNavigation`. |
|
|
442
|
+
| `cachedNavigationCancel` | `{ pathname, startedAt }` | Clears pending cached navigation state. |
|
|
443
|
+
|
|
444
|
+
`cachedNavigationComplete` waits until the cached route is visible and then waits for two animation frames. This makes `duration` closer to "restored and painted" timing rather than just "state changed" timing.
|
|
445
|
+
|
|
446
|
+
## Error handling
|
|
447
|
+
|
|
448
|
+
Cached errored routes are intentionally discarded.
|
|
449
|
+
|
|
450
|
+
```mermaid
|
|
451
|
+
flowchart TD
|
|
452
|
+
route["Current route"]
|
|
453
|
+
routerError{"TanStack match status is error?"}
|
|
454
|
+
boundary["useRouteCacheErrorBoundary"]
|
|
455
|
+
retain["Retain errored pathname"]
|
|
456
|
+
deleteEntry["Delete cached entry"]
|
|
457
|
+
live["Render live route or error boundary"]
|
|
458
|
+
release["Release errored pathname on unmount"]
|
|
459
|
+
|
|
460
|
+
route --> routerError
|
|
461
|
+
routerError -- yes --> deleteEntry --> live
|
|
462
|
+
routerError -- no --> live
|
|
463
|
+
boundary --> retain --> deleteEntry
|
|
464
|
+
boundary --> release
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
There are two error paths:
|
|
468
|
+
|
|
469
|
+
- If TanStack Router reports the current match status as `error`, the cache manager deletes that pathname's cached entry.
|
|
470
|
+
- If an app-level error fallback calls `useRouteCacheErrorBoundary`, the provider increments an errored count for that pathname and deletes its cached entry until the fallback unmounts.
|
|
471
|
+
|
|
472
|
+
While a route is errored, the cache manager bypasses its cached outlet and lets the live route or error boundary render.
|
|
473
|
+
|
|
474
|
+
## Memory model
|
|
475
|
+
|
|
476
|
+
The package does not clone React component state. It keeps selected route trees mounted and hides them. Memory is bounded by the number and size of cached route trees that remain mounted.
|
|
477
|
+
|
|
478
|
+
```mermaid
|
|
479
|
+
flowchart LR
|
|
480
|
+
optIn["routeCache opt in"]
|
|
481
|
+
limits["maxEntries and maxEntriesPerRouteId"]
|
|
482
|
+
scope["cacheScopeKey reset"]
|
|
483
|
+
manual["destroy, destroyAll, invalidateWhere"]
|
|
484
|
+
errors["error cleanup"]
|
|
485
|
+
memory["Retained mounted route trees"]
|
|
486
|
+
|
|
487
|
+
optIn --> memory
|
|
488
|
+
limits --> memory
|
|
489
|
+
scope --> memory
|
|
490
|
+
manual --> memory
|
|
491
|
+
errors --> memory
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The memory controls are:
|
|
495
|
+
|
|
496
|
+
- route opt-in through `staticData.routeCache`,
|
|
497
|
+
- `maxEntries` for global cache size,
|
|
498
|
+
- `maxEntriesPerRouteId` for dynamic routes,
|
|
499
|
+
- `cacheScopeKey` for tenant, user, workspace, or environment resets,
|
|
500
|
+
- `destroy`, `destroyAll`, and `invalidateWhere` for manual invalidation,
|
|
501
|
+
- automatic deletion when a route stops being cacheable or enters an error state.
|
|
502
|
+
|
|
503
|
+
Dynamic routes are the most important case to bound. For routes such as `/customers/$customerId`, every distinct pathname can retain a separate route tree unless `maxEntriesPerRouteId` or manual invalidation removes older entries.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Cache Behavior
|
|
2
|
+
|
|
3
|
+
- Pathnames are normalized before being stored or removed.
|
|
4
|
+
- `maxEntries={0}` disables caching and clears existing cached routes.
|
|
5
|
+
- Hidden cached routes are rendered in off-screen containers and receive active-change events.
|
|
6
|
+
- Cached dynamic routes can grow memory use if every id is retained; use `maxEntriesPerRouteId` for those routes.
|
|
7
|
+
- If a cached destination is restored with an outdated href, the package navigates back to the cached href with `replace: true` and `resetScroll: false`.
|
|
8
|
+
|
|
9
|
+
## Eviction
|
|
10
|
+
|
|
11
|
+
`maxEntries` limits the total number of cached route entries managed by one provider.
|
|
12
|
+
|
|
13
|
+
`maxEntriesPerRouteId` limits cached entries for the same TanStack route id. This is useful for dynamic routes where each pathname can produce a separate cached entry, such as `/customers/$customerId`.
|
|
14
|
+
|
|
15
|
+
When a limit is exceeded, the least recently visible cached route is evicted first. If timestamps tie, pathnames are used as a deterministic fallback.
|
|
16
|
+
|
|
17
|
+
## Scope resets
|
|
18
|
+
|
|
19
|
+
Use `cacheScopeKey` when cached views must not survive a user, tenant, workspace, locale, or environment change.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<RouterCacheProvider cacheScopeKey={workspaceId}>
|
|
23
|
+
<RouterCacheOutlet />
|
|
24
|
+
</RouterCacheProvider>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Changing the scope key clears existing cached route entries for that provider.
|
|
28
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
## `RouterCacheProvider`
|
|
4
|
+
|
|
5
|
+
Owns the route cache and exposes cache state to the rest of the package.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<RouterCacheProvider
|
|
9
|
+
cacheScopeKey={tenantId}
|
|
10
|
+
defaultCachedRoutes={{}}
|
|
11
|
+
maxEntries={10}
|
|
12
|
+
maxEntriesPerRouteId={2}
|
|
13
|
+
>
|
|
14
|
+
<RouterCacheOutlet />
|
|
15
|
+
</RouterCacheProvider>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| `children` | `ReactNode` | Required | The outlet and surrounding UI that can use the cache. |
|
|
21
|
+
| `cacheScopeKey` | `string | number | null` | `"__default__"` | Resets the entire cache when the key changes. Use this for tenant, user, workspace, or environment changes. |
|
|
22
|
+
| `defaultCachedRoutes` | `CachedRoutes` | `{}` | Initial cache data. Most apps do not need this. Entries without `staticData.routeCache: true` are ignored. |
|
|
23
|
+
| `maxEntries` | `number` | `Infinity` | Maximum cached route entries across the provider. `0` disables caching. Non-finite or invalid values are treated as `Infinity`. |
|
|
24
|
+
| `maxEntriesPerRouteId` | `number` | `Infinity` | Maximum cached entries for the same TanStack route id. Useful for dynamic routes such as `/customers/$id`. |
|
|
25
|
+
|
|
26
|
+
When a cache limit is exceeded, the least recently visible cached route is evicted first. If timestamps tie, pathnames are used as a deterministic fallback.
|
|
27
|
+
|
|
28
|
+
## `RouterCacheOutlet`
|
|
29
|
+
|
|
30
|
+
Renders the live TanStack Router outlet and any hidden cached route views.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<RouterCacheOutlet />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Prop | Type | Default | Description |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `children` | `ReactNode` | `undefined` | Optional content rendered after the outlet manager. |
|
|
39
|
+
|
|
40
|
+
Use one `RouterCacheOutlet` inside a `RouterCacheProvider` for the route branch you want to cache.
|
|
41
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Debugging
|
|
2
|
+
|
|
3
|
+
`RouterCacheOutlet` exposes development diagnostics for the cache.
|
|
4
|
+
|
|
5
|
+
In non-production environments, diagnostics are available at:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
window.__TANSTACK_ROUTER_CACHE_DEBUG__
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Debug API
|
|
12
|
+
|
|
13
|
+
| Field | Type | Description |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `getSnapshot` | `() => RouterCacheDebugSnapshot` | Returns the last recorded snapshot. |
|
|
16
|
+
| `refresh` | `() => RouterCacheDebugSnapshot` | Recomputes and returns a snapshot. |
|
|
17
|
+
| `lastSnapshot` | `RouterCacheDebugSnapshot | undefined` | Most recent snapshot. |
|
|
18
|
+
| `setWarningThreshold` | `(nextThreshold?: number | null) => void` | Sets a development warning threshold for cached route count. |
|
|
19
|
+
| `warningThreshold` | `number | null | undefined` | Current warning threshold. |
|
|
20
|
+
|
|
21
|
+
## Snapshot fields
|
|
22
|
+
|
|
23
|
+
| Field | Type | Description |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `totalCachedRouteCount` | `number` | Total cached route count. |
|
|
26
|
+
| `cachedRoutePathnames` | `string[]` | Cached route pathnames. |
|
|
27
|
+
| `dynamicLookingRouteCount` | `number` | Count of cached routes with id-like dynamic segments. |
|
|
28
|
+
| `dynamicLookingRoutePathnames` | `string[]` | Cached pathnames that look dynamic. |
|
|
29
|
+
| `hiddenCachedRouteCount` | `number` | Cached routes that are not currently visible. |
|
|
30
|
+
| `hiddenContainerCount` | `number` | Hidden DOM containers managed by the package. |
|
|
31
|
+
| `visiblePathname` | `string` | Current visible cached pathname. |
|