tanstack-router-cache 0.1.7 → 0.1.9

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.
Files changed (50) hide show
  1. package/dist/components/cached-outlet.cjs +12 -0
  2. package/dist/components/cached-outlet.js +12 -0
  3. package/dist/components/off-screen-in.cjs +130 -0
  4. package/dist/components/off-screen-in.js +130 -0
  5. package/dist/components/off-screen.cjs +8 -0
  6. package/dist/components/off-screen.js +8 -0
  7. package/dist/components/restore-cached-href.cjs +28 -0
  8. package/dist/components/restore-cached-href.js +28 -0
  9. package/dist/components/route-cache-manager.cjs +485 -0
  10. package/dist/components/route-cache-manager.js +485 -0
  11. package/dist/components/router-cache-outlet.cjs +9 -0
  12. package/dist/components/router-cache-outlet.js +9 -0
  13. package/dist/contexts/router-cache.cjs +237 -0
  14. package/dist/contexts/router-cache.d.ts +40 -0
  15. package/dist/contexts/router-cache.js +235 -0
  16. package/dist/dom/dismiss-transient-ui.cjs +230 -0
  17. package/dist/dom/dismiss-transient-ui.js +228 -0
  18. package/dist/hooks/use-event-listener.cjs +76 -0
  19. package/dist/hooks/use-event-listener.js +76 -0
  20. package/dist/hooks/use-route-cache-active.cjs +19 -0
  21. package/dist/hooks/use-route-cache-active.js +19 -0
  22. package/dist/hooks/use-route-cache-activity.cjs +12 -0
  23. package/dist/hooks/use-route-cache-activity.js +12 -0
  24. package/dist/hooks/use-route-cache-effect.cjs +38 -0
  25. package/dist/hooks/use-route-cache-effect.js +38 -0
  26. package/dist/hooks/use-route-cache-error-boundary.cjs +23 -0
  27. package/dist/hooks/use-route-cache-error-boundary.js +23 -0
  28. package/dist/hooks/use-route-cache-navigation.cjs +36 -0
  29. package/dist/hooks/use-route-cache-navigation.js +36 -0
  30. package/dist/hooks/use-router-cache-debug.cjs +85 -0
  31. package/dist/hooks/use-router-cache-debug.js +85 -0
  32. package/dist/hooks/use-router-cache.cjs +32 -0
  33. package/dist/hooks/use-router-cache.js +32 -0
  34. package/dist/hooks/use-update.cjs +8 -0
  35. package/dist/hooks/use-update.js +8 -0
  36. package/dist/index.cjs +18 -1402
  37. package/dist/index.d.ts +9 -1
  38. package/dist/index.js +10 -1395
  39. package/dist/pathname.cjs +8 -0
  40. package/dist/pathname.js +8 -0
  41. package/dist/route-cache-static-data.cjs +43 -0
  42. package/dist/route-cache-static-data.d.ts +45 -0
  43. package/dist/route-cache-static-data.js +41 -0
  44. package/dist/types.d.ts +28 -0
  45. package/docs/architecture.md +8 -5
  46. package/docs/cache-behavior.md +17 -0
  47. package/docs/components.md +1 -2
  48. package/docs/getting-started.md +38 -3
  49. package/docs/types.md +6 -0
  50. package/package.json +2 -2
@@ -0,0 +1,8 @@
1
+ //#region src/pathname.ts
2
+ const TRAILING_SLASHES_REGEX = /\/+$/u;
3
+ function normalizeCachedRoutePathname(pathname) {
4
+ if (pathname === "/") return pathname;
5
+ return pathname.replace(TRAILING_SLASHES_REGEX, "");
6
+ }
7
+ //#endregion
8
+ exports.normalizeCachedRoutePathname = normalizeCachedRoutePathname;
@@ -0,0 +1,8 @@
1
+ //#region src/pathname.ts
2
+ const TRAILING_SLASHES_REGEX = /\/+$/u;
3
+ function normalizeCachedRoutePathname(pathname) {
4
+ if (pathname === "/") return pathname;
5
+ return pathname.replace(TRAILING_SLASHES_REGEX, "");
6
+ }
7
+ //#endregion
8
+ export { normalizeCachedRoutePathname };
@@ -0,0 +1,43 @@
1
+ //#region src/route-cache-static-data.ts
2
+ const DEFAULT_ROUTE_CACHE_MAX_AGE = Number.POSITIVE_INFINITY;
3
+ function getRouteCacheOptions(staticData) {
4
+ const routeCache = staticData?.routeCache;
5
+ if (routeCache === true) return {};
6
+ if (routeCache && typeof routeCache === "object") return routeCache;
7
+ }
8
+ /**
9
+ * Builds route options for a cacheable TanStack Router route.
10
+ *
11
+ * TanStack Router loader-cache options are returned at the route-option level,
12
+ * while route-cache-specific options are stored under `staticData.routeCache`.
13
+ */
14
+ function defineRouteCache(options = {}) {
15
+ const { gcTime, maxAge, preloadStaleTime, staleTime } = options;
16
+ return {
17
+ ...gcTime === void 0 ? {} : { gcTime },
18
+ ...preloadStaleTime === void 0 ? {} : { preloadStaleTime },
19
+ ...staleTime === void 0 ? {} : { staleTime },
20
+ staticData: { routeCache: maxAge === void 0 ? true : { maxAge } }
21
+ };
22
+ }
23
+ function isRouteCacheEnabled(staticData) {
24
+ return Boolean(getRouteCacheOptions(staticData));
25
+ }
26
+ function normalizeRouteCacheMaxAge(maxAge) {
27
+ if (typeof maxAge !== "number" || Number.isNaN(maxAge)) return DEFAULT_ROUTE_CACHE_MAX_AGE;
28
+ if (!Number.isFinite(maxAge)) return DEFAULT_ROUTE_CACHE_MAX_AGE;
29
+ return Math.max(maxAge, 0);
30
+ }
31
+ function getRouteCacheMaxAge(staticData) {
32
+ return normalizeRouteCacheMaxAge(getRouteCacheOptions(staticData)?.maxAge);
33
+ }
34
+ function isCachedRouteStale(route, now = Date.now()) {
35
+ if (!route) return false;
36
+ const maxAge = getRouteCacheMaxAge(route.staticData);
37
+ if (maxAge === DEFAULT_ROUTE_CACHE_MAX_AGE) return false;
38
+ return now - (route.createdAt ?? now) > maxAge;
39
+ }
40
+ //#endregion
41
+ exports.defineRouteCache = defineRouteCache;
42
+ exports.isCachedRouteStale = isCachedRouteStale;
43
+ exports.isRouteCacheEnabled = isRouteCacheEnabled;
@@ -0,0 +1,45 @@
1
+ import type { StaticDataRouteOption } from "@tanstack/react-router";
2
+ import type { RouteCacheOptions } from "./types";
3
+ type CachedRouteTiming = {
4
+ createdAt?: number;
5
+ staticData: StaticDataRouteOption;
6
+ };
7
+ /**
8
+ * Builds route options for a cacheable TanStack Router route.
9
+ *
10
+ * TanStack Router loader-cache options are returned at the route-option level,
11
+ * while route-cache-specific options are stored under `staticData.routeCache`.
12
+ */
13
+ export declare function defineRouteCache(options?: RouteCacheOptions & {
14
+ /**
15
+ * TanStack Router loader garbage-collection time, in milliseconds.
16
+ *
17
+ * This is returned as a top-level route option.
18
+ */
19
+ gcTime?: number;
20
+ /**
21
+ * TanStack Router preload freshness time, in milliseconds.
22
+ *
23
+ * This is returned as a top-level route option.
24
+ */
25
+ preloadStaleTime?: number;
26
+ /**
27
+ * TanStack Router loader freshness time, in milliseconds.
28
+ *
29
+ * This is returned as a top-level route option and does not control the
30
+ * retained route view lifetime. Use `maxAge` for that.
31
+ */
32
+ staleTime?: number;
33
+ }): {
34
+ staticData: {
35
+ routeCache: boolean | {
36
+ maxAge: number;
37
+ };
38
+ };
39
+ staleTime?: number | undefined;
40
+ preloadStaleTime?: number | undefined;
41
+ gcTime?: number | undefined;
42
+ };
43
+ export declare function isRouteCacheEnabled(staticData: StaticDataRouteOption | undefined): boolean;
44
+ export declare function isCachedRouteStale(route: CachedRouteTiming | undefined, now?: number): boolean;
45
+ export {};
@@ -0,0 +1,41 @@
1
+ //#region src/route-cache-static-data.ts
2
+ const DEFAULT_ROUTE_CACHE_MAX_AGE = Number.POSITIVE_INFINITY;
3
+ function getRouteCacheOptions(staticData) {
4
+ const routeCache = staticData?.routeCache;
5
+ if (routeCache === true) return {};
6
+ if (routeCache && typeof routeCache === "object") return routeCache;
7
+ }
8
+ /**
9
+ * Builds route options for a cacheable TanStack Router route.
10
+ *
11
+ * TanStack Router loader-cache options are returned at the route-option level,
12
+ * while route-cache-specific options are stored under `staticData.routeCache`.
13
+ */
14
+ function defineRouteCache(options = {}) {
15
+ const { gcTime, maxAge, preloadStaleTime, staleTime } = options;
16
+ return {
17
+ ...gcTime === void 0 ? {} : { gcTime },
18
+ ...preloadStaleTime === void 0 ? {} : { preloadStaleTime },
19
+ ...staleTime === void 0 ? {} : { staleTime },
20
+ staticData: { routeCache: maxAge === void 0 ? true : { maxAge } }
21
+ };
22
+ }
23
+ function isRouteCacheEnabled(staticData) {
24
+ return Boolean(getRouteCacheOptions(staticData));
25
+ }
26
+ function normalizeRouteCacheMaxAge(maxAge) {
27
+ if (typeof maxAge !== "number" || Number.isNaN(maxAge)) return DEFAULT_ROUTE_CACHE_MAX_AGE;
28
+ if (!Number.isFinite(maxAge)) return DEFAULT_ROUTE_CACHE_MAX_AGE;
29
+ return Math.max(maxAge, 0);
30
+ }
31
+ function getRouteCacheMaxAge(staticData) {
32
+ return normalizeRouteCacheMaxAge(getRouteCacheOptions(staticData)?.maxAge);
33
+ }
34
+ function isCachedRouteStale(route, now = Date.now()) {
35
+ if (!route) return false;
36
+ const maxAge = getRouteCacheMaxAge(route.staticData);
37
+ if (maxAge === DEFAULT_ROUTE_CACHE_MAX_AGE) return false;
38
+ return now - (route.createdAt ?? now) > maxAge;
39
+ }
40
+ //#endregion
41
+ export { defineRouteCache, isCachedRouteStale, isRouteCacheEnabled };
package/dist/types.d.ts CHANGED
@@ -1,10 +1,38 @@
1
+ /** Visibility mode for a cached route container. */
1
2
  export type ActivityMode = "visible" | "hidden";
3
+ /** Route-cache options stored in TanStack Router `staticData.routeCache`. */
4
+ export type RouteCacheOptions = {
5
+ /**
6
+ * Maximum age, in milliseconds, for a retained route view.
7
+ *
8
+ * Expired cached views are not restored. This only controls the retained
9
+ * mounted view; use TanStack Router's `staleTime`, `preloadStaleTime`, and
10
+ * `gcTime` route options for loader-data caching.
11
+ *
12
+ * @defaultValue `Infinity`
13
+ */
14
+ maxAge?: number;
15
+ };
16
+ /**
17
+ * Static route-cache opt-in value.
18
+ *
19
+ * Use `true` to keep the route view cached with default behavior, or an object
20
+ * when the retained view needs route-cache-specific options.
21
+ */
22
+ export type RouteCacheStaticOption = boolean | RouteCacheOptions;
23
+ /** Emitted when navigation to a ready cached route begins. */
2
24
  export type RouteCacheNavigationStart = {
25
+ /** Normalized pathname for the cached route being restored. */
3
26
  pathname: string;
27
+ /** `performance.now()` timestamp for the navigation start. */
4
28
  startedAt: number;
5
29
  };
30
+ /** Emitted after a cached route has become visible and painted. */
6
31
  export type RouteCacheNavigationComplete = RouteCacheNavigationStart & {
32
+ /** Total elapsed time from start to painted, in milliseconds. */
7
33
  duration: number;
34
+ /** `performance.now()` timestamp after the visible route painted. */
8
35
  paintedAt: number;
36
+ /** `performance.now()` timestamp when the cached route became visible. */
9
37
  visibleAt: number;
10
38
  };
@@ -45,12 +45,12 @@ flowchart TD
45
45
 
46
46
  ## Route lifecycle
47
47
 
48
- A route becomes cacheable only after the current match is resolved, successful, and marked with `staticData.routeCache: true`.
48
+ A route becomes cacheable only after the current match is resolved, successful, and marked with enabled `staticData.routeCache`.
49
49
 
50
50
  ```mermaid
51
51
  stateDiagram-v2
52
52
  [*] --> LiveRoute: normal TanStack render
53
- LiveRoute --> WaitingForReady: routeCache true
53
+ LiveRoute --> WaitingForReady: routeCache enabled
54
54
  WaitingForReady --> CachedReady: match success and snapshot stored
55
55
  CachedReady --> VisibleCached: pathname is visible
56
56
  VisibleCached --> HiddenCached: navigate away
@@ -60,7 +60,7 @@ stateDiagram-v2
60
60
  Evicted --> [*]
61
61
  ```
62
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.
63
+ In normal usage, a cached route alternates between `visible` and `hidden`. It leaves the cache when a limit evicts it, its route `maxAge` expires, the app invalidates it, the route stops being cacheable, or an error boundary marks it as failed.
64
64
 
65
65
  ## Provider state
66
66
 
@@ -147,7 +147,7 @@ classDiagram
147
147
  | `href` | Full route href, including search and hash when available. Used for restoration. |
148
148
  | `lastVisibleAt` | Last time the entry became visible. Primary eviction timestamp. |
149
149
  | `routeId` | TanStack route id. Used by `maxEntriesPerRouteId`. |
150
- | `staticData` | Route static data. The route is cacheable when `routeCache` is `true`. |
150
+ | `staticData` | Route static data. The route is cacheable when `routeCache` is `true` or an options object. |
151
151
  | `matchId` | Match id used to render the cached route with TanStack Router's `Match`. |
152
152
  | `routerSnapshot` | Router-like object with isolated snapshot stores used by the cached route tree. |
153
153
  | `ready` | Marks that the route has a complete snapshot and can be rendered from cache. |
@@ -204,7 +204,7 @@ flowchart TD
204
204
  staticData["Find deepest routeCache static data"]
205
205
  errored{"Current route errored?"}
206
206
  resolved{"Match resolved?"}
207
- cacheable{"routeCache true?"}
207
+ cacheable{"routeCache enabled?"}
208
208
  ready{"Match successful?"}
209
209
  write["Create or refresh cache entry"]
210
210
  deleteEntry["Delete cache entry"]
@@ -225,6 +225,8 @@ flowchart TD
225
225
 
226
226
  The current entry being written is protected during limit enforcement so the route that just became ready is not immediately evicted.
227
227
 
228
+ Cached entries with a route `maxAge` are treated as expired when their age exceeds that value. Expired entries are not rendered from the snapshot and are removed on the next cache update.
229
+
228
230
  ## Visible pathname
229
231
 
230
232
  The manager tracks three pathnames:
@@ -496,6 +498,7 @@ The memory controls are:
496
498
  - route opt-in through `staticData.routeCache`,
497
499
  - `maxEntries` for global cache size,
498
500
  - `maxEntriesPerRouteId` for dynamic routes,
501
+ - route-level `maxAge` for expiring retained views,
499
502
  - `cacheScopeKey` for tenant, user, workspace, or environment resets,
500
503
  - `destroy`, `destroyAll`, and `invalidateWhere` for manual invalidation,
501
504
  - automatic deletion when a route stops being cacheable or enters an error state.
@@ -5,6 +5,7 @@
5
5
  - Hidden cached routes are rendered in off-screen containers and receive active-change events.
6
6
  - Cached dynamic routes can grow memory use if every id is retained; use `maxEntriesPerRouteId` for those routes.
7
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
+ - A route can use `staticData.routeCache.maxAge` to stop restoring an old retained view after a fixed age.
8
9
 
9
10
  ## Eviction
10
11
 
@@ -26,3 +27,19 @@ Use `cacheScopeKey` when cached views must not survive a user, tenant, workspace
26
27
 
27
28
  Changing the scope key clears existing cached route entries for that provider.
28
29
 
30
+ ## Route max age
31
+
32
+ `maxAge` controls the lifetime of this package's retained route view. It does not replace TanStack Router's top-level `staleTime`, `preloadStaleTime`, or `gcTime` options.
33
+
34
+ ```tsx
35
+ export const Route = createFileRoute("/customers")({
36
+ staticData: {
37
+ routeCache: {
38
+ maxAge: 10 * 60_000,
39
+ },
40
+ },
41
+ component: CustomersPage,
42
+ });
43
+ ```
44
+
45
+ When a cached entry is older than `maxAge`, the cache manager does not restore it. The live route renders and the expired cache entry is removed on the next cache update.
@@ -19,7 +19,7 @@ Owns the route cache and exposes cache state to the rest of the package.
19
19
  | --- | --- | --- | --- |
20
20
  | `children` | `ReactNode` | Required | The outlet and surrounding UI that can use the cache. |
21
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. |
22
+ | `defaultCachedRoutes` | `CachedRoutes` | `{}` | Initial cache data. Most apps do not need this. Entries without enabled `staticData.routeCache` or entries older than their `maxAge` are ignored. |
23
23
  | `maxEntries` | `number` | `Infinity` | Maximum cached route entries across the provider. `0` disables caching. Non-finite or invalid values are treated as `Infinity`. |
24
24
  | `maxEntriesPerRouteId` | `number` | `Infinity` | Maximum cached entries for the same TanStack route id. Useful for dynamic routes such as `/customers/$id`. |
25
25
 
@@ -38,4 +38,3 @@ Renders the live TanStack Router outlet and any hidden cached route views.
38
38
  | `children` | `ReactNode` | `undefined` | Optional content rendered after the outlet manager. |
39
39
 
40
40
  Use one `RouterCacheOutlet` inside a `RouterCacheProvider` for the route branch you want to cache.
41
-
@@ -70,7 +70,25 @@ export const Route = createFileRoute("/customers")({
70
70
  });
71
71
  ```
72
72
 
73
- Routes without `routeCache: true` are rendered and unmounted by TanStack Router normally.
73
+ Routes without enabled `routeCache` are rendered and unmounted by TanStack Router normally.
74
+
75
+ When you also need TanStack Router loader cache options, use `defineRouteCache`. It keeps Router options at the top level of the route config while adding the `staticData.routeCache` opt-in for this package.
76
+
77
+ ```tsx
78
+ import { defineRouteCache } from "tanstack-router-cache";
79
+
80
+ export const Route = createFileRoute("/customers")({
81
+ ...defineRouteCache({
82
+ gcTime: Number.POSITIVE_INFINITY,
83
+ maxAge: 10 * 60_000,
84
+ preloadStaleTime: 30_000,
85
+ staleTime: Number.POSITIVE_INFINITY,
86
+ }),
87
+ component: CustomersPage,
88
+ });
89
+ ```
90
+
91
+ In that example, `staleTime`, `preloadStaleTime`, and `gcTime` are TanStack Router route options. `maxAge` is the route-cache view lifetime; after that age the cached view is not restored and the live route renders again.
74
92
 
75
93
  ### 3. Pause route work while hidden
76
94
 
@@ -123,7 +141,7 @@ export const Route = createFileRoute("/customers")({
123
141
  });
124
142
  ```
125
143
 
126
- Routes without `routeCache: true` are rendered normally and are removed when TanStack Router unmounts them.
144
+ Routes without enabled `routeCache` are rendered normally and are removed when TanStack Router unmounts them.
127
145
 
128
146
  ## Route static data
129
147
 
@@ -132,7 +150,11 @@ The package augments TanStack Router's `StaticDataRouteOption` type:
132
150
  ```ts
133
151
  declare module "@tanstack/react-router" {
134
152
  interface StaticDataRouteOption {
135
- routeCache?: boolean;
153
+ routeCache?:
154
+ | boolean
155
+ | {
156
+ maxAge?: number;
157
+ };
136
158
  }
137
159
  }
138
160
  ```
@@ -148,4 +170,17 @@ export const Route = createFileRoute("/reports")({
148
170
  });
149
171
  ```
150
172
 
173
+ Use an object when the retained route view should expire independently from TanStack Router's loader cache:
174
+
175
+ ```tsx
176
+ export const Route = createFileRoute("/reports")({
177
+ staticData: {
178
+ routeCache: {
179
+ maxAge: 5 * 60_000,
180
+ },
181
+ },
182
+ component: ReportsPage,
183
+ });
184
+ ```
185
+
151
186
  If multiple child matches are present, the cache manager checks child route static data from deepest to shallowest and uses the deepest retained route data.
package/docs/types.md CHANGED
@@ -5,6 +5,12 @@
5
5
  ```ts
6
6
  export type ActivityMode = "visible" | "hidden";
7
7
 
8
+ export type RouteCacheOptions = {
9
+ maxAge?: number;
10
+ };
11
+
12
+ export type RouteCacheStaticOption = boolean | RouteCacheOptions;
13
+
8
14
  export type RouteCacheNavigationStart = {
9
15
  pathname: string;
10
16
  startedAt: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanstack-router-cache",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Route view caching for TanStack Router.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -68,7 +68,7 @@
68
68
  "@types/react-dom": "19.2.3",
69
69
  "react": "19.2.7",
70
70
  "react-dom": "19.2.7",
71
- "rolldown": "1.1.1",
71
+ "rolldown": "1.1.2",
72
72
  "typescript": "6.0.3"
73
73
  }
74
74
  }