tanstack-router-cache 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -2,21 +2,21 @@
2
2
 
3
3
  Route view caching for [`@tanstack/react-router`](https://tanstack.com/router).
4
4
 
5
- `tanstack-router-cache` keeps selected route trees mounted while they are hidden, then restores them when the user navigates back. Use it for tab-like workflows, long forms, filtered lists, scroll positions, and page state that should survive route changes without moving everything into global state.
5
+ Keep selected route trees mounted while they are hidden, then restore them when the user navigates back.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- bun add tanstack-router-cache
10
+ npm install tanstack-router-cache
11
11
  ```
12
12
 
13
13
  ```sh
14
- npm install tanstack-router-cache
14
+ bun add tanstack-router-cache
15
15
  ```
16
16
 
17
- ## Consumer requirements
17
+ ## Requirements
18
18
 
19
- Your app should already use React and TanStack Router. This package keeps them as peer dependencies so it does not install a second router or React runtime.
19
+ Your app should already install these packages:
20
20
 
21
21
  | Package | Supported versions |
22
22
  | --- | --- |
@@ -24,27 +24,23 @@ Your app should already use React and TanStack Router. This package keeps them a
24
24
  | `react-dom` | Match your React version. |
25
25
  | `@tanstack/react-router` | `>=1.168.14 <2.0.0` |
26
26
 
27
- ## Maintenance
28
-
29
- This package is intended to stay compatible with current TanStack Router 1.x releases. The peer dependency floor is tested against the oldest version supported by the current implementation, while development tracks the latest compatible TanStack Router version.
30
-
31
27
  ## Usage
32
28
 
33
- Wrap the route tree that should support retention:
29
+ Wrap your route outlet once:
34
30
 
35
31
  ```tsx
36
32
  import { RouterCacheOutlet, RouterCacheProvider } from "tanstack-router-cache";
37
33
 
38
34
  export function RootRoute() {
39
35
  return (
40
- <RouterCacheProvider maxEntries={8} maxEntriesPerRouteId={2}>
36
+ <RouterCacheProvider>
41
37
  <RouterCacheOutlet />
42
38
  </RouterCacheProvider>
43
39
  );
44
40
  }
45
41
  ```
46
42
 
47
- Enable caching on a route:
43
+ Enable caching on any route that should stay mounted:
48
44
 
49
45
  ```tsx
50
46
  export const Route = createFileRoute("/customers")({
@@ -55,29 +51,15 @@ export const Route = createFileRoute("/customers")({
55
51
  });
56
52
  ```
57
53
 
58
- Run work only while a retained route is visible:
54
+ For the full API, see [docs](./docs).
59
55
 
60
- ```tsx
61
- import { useRouteCacheEffect } from "tanstack-router-cache";
62
-
63
- function CustomersPage() {
64
- useRouteCacheEffect(() => {
65
- const controller = new AbortController();
66
-
67
- return () => {
68
- controller.abort();
69
- };
70
- }, []);
71
-
72
- return <CustomersTable />;
73
- }
74
- ```
56
+ ## Examples
75
57
 
76
- More usage details are in [docs/usage.md](./docs/usage.md).
58
+ - [Demo app](https://github.com/santiago-ramos-02/tanstack-router-cache/tree/main/examples/demo): one Vite app with a basic flow for most users and a power-user flow for edge cases.
77
59
 
78
60
  ## Acknowledgements
79
61
 
80
- This project originated from [`hemengke1997/tanstack-router-keepalive`](https://github.com/hemengke1997/tanstack-router-keepalive). The implementation has since diverged substantially, including current TanStack Router compatibility, cache limits, error handling, navigation lifecycle instrumentation, dependency updates, and memory-focused eviction.
62
+ This project originated from [`hemengke1997/tanstack-router-keepalive`](https://github.com/hemengke1997/tanstack-router-keepalive), then diverged with current TanStack Router compatibility, a different API, cache limits, error handling, navigation lifecycle tools, dependency updates, and memory-focused eviction.
81
63
 
82
64
  ## License
83
65
 
package/dist/index.cjs CHANGED
@@ -131,8 +131,9 @@ function RouterCacheProviderScope({ children, defaultCachedRoutes = EMPTY_CACHED
131
131
  maxEntries: normalizeLimit(maxEntries),
132
132
  maxEntriesPerRouteId: normalizeLimit(maxEntriesPerRouteId)
133
133
  });
134
- const initialCachedRoutes = (0, react.useRef)(applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set()));
135
- const [cachedRoutes, setCachedRoutes] = (0, react.useState)(() => initialCachedRoutes.current);
134
+ const initialCachedRoutesRef = (0, react.useRef)(null);
135
+ if (initialCachedRoutesRef.current === null) initialCachedRoutesRef.current = applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set());
136
+ const [cachedRoutes, setCachedRoutes] = (0, react.useState)(() => initialCachedRoutesRef.current ?? EMPTY_CACHED_ROUTES);
136
137
  const [erroredRouteCounts, setErroredRouteCounts] = (0, react.useState)({});
137
138
  const updateCachedRoutes = (key, value) => {
138
139
  setCachedRoutes((state) => getNextCachedRoutesState({
@@ -340,7 +341,8 @@ function isProduction() {
340
341
  function useRouterCacheDebug(cachedRoutes, visiblePathname) {
341
342
  const warningThresholdRef = (0, react.useRef)(null);
342
343
  const lastWarnedCountRef = (0, react.useRef)(null);
343
- const lastSnapshotRef = (0, react.useRef)(buildSnapshot(cachedRoutes, visiblePathname));
344
+ const lastSnapshotRef = (0, react.useRef)(null);
345
+ if (lastSnapshotRef.current === null) lastSnapshotRef.current = buildSnapshot(cachedRoutes, visiblePathname);
344
346
  const snapshot = (0, react.useMemo)(() => buildSnapshot(cachedRoutes, visiblePathname), [cachedRoutes, visiblePathname]);
345
347
  (0, react.useEffect)(() => {
346
348
  if (isProduction()) return;
@@ -351,7 +353,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
351
353
  return lastSnapshotRef.current;
352
354
  };
353
355
  const api = {
354
- getSnapshot: () => lastSnapshotRef.current,
356
+ getSnapshot: () => lastSnapshotRef.current ?? snapshot,
355
357
  lastSnapshot: snapshot,
356
358
  refresh,
357
359
  setWarningThreshold: (nextThreshold) => {
@@ -369,10 +371,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
369
371
  warningThreshold: warningThresholdRef.current
370
372
  };
371
373
  const warningThreshold = warningThresholdRef.current;
372
- if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) {
373
- lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
374
- console.warn("[tanstack-router-cache] cached route count exceeded threshold", nextSnapshot);
375
- }
374
+ if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
376
375
  });
377
376
  return () => {
378
377
  globalThis.cancelAnimationFrame(rafId);
@@ -623,6 +622,30 @@ function dismissTransientUi(container, pathname) {
623
622
  //#endregion
624
623
  //#region src/components/off-screen-in.tsx
625
624
  const windowScrollPositions = /* @__PURE__ */ new Map();
625
+ const IMMEDIATE_SCROLL_RESTORE_DELAY = 0;
626
+ const EARLY_SCROLL_RESTORE_DELAY = 80;
627
+ const MIDDLE_SCROLL_RESTORE_DELAY = 240;
628
+ const LATE_SCROLL_RESTORE_DELAY = 600;
629
+ const FINAL_SCROLL_RESTORE_DELAY = 1e3;
630
+ const SCROLL_TRACKING_INTERVAL_MS = 200;
631
+ const SCROLL_KEYS = new Set([
632
+ " ",
633
+ "ArrowDown",
634
+ "ArrowLeft",
635
+ "ArrowRight",
636
+ "ArrowUp",
637
+ "End",
638
+ "Home",
639
+ "PageDown",
640
+ "PageUp"
641
+ ]);
642
+ const SCROLL_RESTORE_DELAYS = [
643
+ IMMEDIATE_SCROLL_RESTORE_DELAY,
644
+ EARLY_SCROLL_RESTORE_DELAY,
645
+ MIDDLE_SCROLL_RESTORE_DELAY,
646
+ LATE_SCROLL_RESTORE_DELAY,
647
+ FINAL_SCROLL_RESTORE_DELAY
648
+ ];
626
649
  function OffScreenIn(props) {
627
650
  const { mode, children, containerRef, pathname } = props;
628
651
  const localContainerRef = (0, react.useRef)(null);
@@ -653,26 +676,61 @@ function OffScreenIn(props) {
653
676
  y: globalThis.scrollY
654
677
  });
655
678
  };
656
- if (mode === "hidden") {
657
- saveScrollPosition();
658
- return;
659
- }
679
+ if (mode === "hidden") return;
660
680
  const savedPosition = windowScrollPositions.get(pathname);
661
- const handleScroll = () => saveScrollPosition();
681
+ let userRequestedScroll = false;
682
+ let scrollTrackingIntervalId;
683
+ const startScrollTracking = () => {
684
+ scrollTrackingIntervalId ??= globalThis.setInterval(saveScrollPosition, SCROLL_TRACKING_INTERVAL_MS);
685
+ };
686
+ const handleScroll = () => {
687
+ if (!savedPosition || userRequestedScroll) saveScrollPosition();
688
+ };
689
+ const markUserRequestedScroll = () => {
690
+ userRequestedScroll = true;
691
+ };
692
+ const handleKeyDown = (event) => {
693
+ if (SCROLL_KEYS.has(event.key)) markUserRequestedScroll();
694
+ };
662
695
  globalThis.addEventListener("scroll", handleScroll, { passive: true });
696
+ globalThis.addEventListener("wheel", markUserRequestedScroll, { passive: true });
697
+ globalThis.addEventListener("touchstart", markUserRequestedScroll, { passive: true });
698
+ globalThis.addEventListener("pointerdown", markUserRequestedScroll, { passive: true });
699
+ globalThis.addEventListener("keydown", handleKeyDown);
663
700
  if (!savedPosition) {
701
+ startScrollTracking();
664
702
  saveScrollPosition();
665
- return () => globalThis.removeEventListener("scroll", handleScroll);
703
+ return () => {
704
+ if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
705
+ globalThis.removeEventListener("scroll", handleScroll);
706
+ globalThis.removeEventListener("wheel", markUserRequestedScroll);
707
+ globalThis.removeEventListener("touchstart", markUserRequestedScroll);
708
+ globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
709
+ globalThis.removeEventListener("keydown", handleKeyDown);
710
+ };
666
711
  }
712
+ const restoreScrollPosition = () => {
713
+ if (userRequestedScroll) return;
714
+ globalThis.scrollTo(savedPosition.x, savedPosition.y);
715
+ saveScrollPosition();
716
+ startScrollTracking();
717
+ };
718
+ const timeoutIds = [];
667
719
  let rafId = globalThis.requestAnimationFrame(() => {
668
720
  rafId = globalThis.requestAnimationFrame(() => {
669
- globalThis.scrollTo(savedPosition.x, savedPosition.y);
670
- saveScrollPosition();
721
+ restoreScrollPosition();
722
+ for (const delay of SCROLL_RESTORE_DELAYS) timeoutIds.push(globalThis.setTimeout(restoreScrollPosition, delay));
671
723
  });
672
724
  });
673
725
  return () => {
674
726
  globalThis.cancelAnimationFrame(rafId);
727
+ for (const timeoutId of timeoutIds) globalThis.clearTimeout(timeoutId);
728
+ if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
675
729
  globalThis.removeEventListener("scroll", handleScroll);
730
+ globalThis.removeEventListener("wheel", markUserRequestedScroll);
731
+ globalThis.removeEventListener("touchstart", markUserRequestedScroll);
732
+ globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
733
+ globalThis.removeEventListener("keydown", handleKeyDown);
676
734
  };
677
735
  }, [mode, pathname]);
678
736
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Activity, {
@@ -742,15 +800,8 @@ function getLiveRouterMethodDescriptors(router) {
742
800
  return [[methodName, { value: method.bind(router) }]];
743
801
  }));
744
802
  }
745
- function isRouterSearch(value) {
746
- return typeof value === "object" && value !== null;
747
- }
748
803
  function toRouterLocation(location) {
749
- return {
750
- href: location.href,
751
- pathname: location.pathname,
752
- search: isRouterSearch(location.search) ? location.search : void 0
753
- };
804
+ return { ...location };
754
805
  }
755
806
  function isReadyCachedRoute(route) {
756
807
  return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.matchId && route.routerSnapshot);
@@ -962,7 +1013,8 @@ function RouteCacheManager() {
962
1013
  const pendingCachedNavigationRef = (0, react.useRef)(null);
963
1014
  const previousPathnameRef = (0, react.useRef)(void 0);
964
1015
  const previousHrefRef = (0, react.useRef)(void 0);
965
- const previousRouteCacheModesRef = (0, react.useRef)(/* @__PURE__ */ new Map());
1016
+ const previousRouteCacheModesRef = (0, react.useRef)(null);
1017
+ if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
966
1018
  const previousVisiblePathnameRef = (0, react.useRef)(void 0);
967
1019
  const routerLocation = (0, _tanstack_react_router.useRouterState)({ select: (state) => toRouterLocation(state.location) });
968
1020
  const routerHref = routerLocation.href;
@@ -1058,6 +1110,7 @@ function RouteCacheManager() {
1058
1110
  visiblePathname
1059
1111
  ]);
1060
1112
  (0, react.useLayoutEffect)(() => {
1113
+ if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1061
1114
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1062
1115
  cachedRoutes,
1063
1116
  eventListener,
package/dist/index.js CHANGED
@@ -130,8 +130,9 @@ function RouterCacheProviderScope({ children, defaultCachedRoutes = EMPTY_CACHED
130
130
  maxEntries: normalizeLimit(maxEntries),
131
131
  maxEntriesPerRouteId: normalizeLimit(maxEntriesPerRouteId)
132
132
  });
133
- const initialCachedRoutes = useRef(applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set()));
134
- const [cachedRoutes, setCachedRoutes] = useState(() => initialCachedRoutes.current);
133
+ const initialCachedRoutesRef = useRef(null);
134
+ if (initialCachedRoutesRef.current === null) initialCachedRoutesRef.current = applyCachedRouteLimits(filterRouterCacheRoutes(defaultCachedRoutes), cacheConfigRef.current, /* @__PURE__ */ new Set());
135
+ const [cachedRoutes, setCachedRoutes] = useState(() => initialCachedRoutesRef.current ?? EMPTY_CACHED_ROUTES);
135
136
  const [erroredRouteCounts, setErroredRouteCounts] = useState({});
136
137
  const updateCachedRoutes = (key, value) => {
137
138
  setCachedRoutes((state) => getNextCachedRoutesState({
@@ -339,7 +340,8 @@ function isProduction() {
339
340
  function useRouterCacheDebug(cachedRoutes, visiblePathname) {
340
341
  const warningThresholdRef = useRef(null);
341
342
  const lastWarnedCountRef = useRef(null);
342
- const lastSnapshotRef = useRef(buildSnapshot(cachedRoutes, visiblePathname));
343
+ const lastSnapshotRef = useRef(null);
344
+ if (lastSnapshotRef.current === null) lastSnapshotRef.current = buildSnapshot(cachedRoutes, visiblePathname);
343
345
  const snapshot = useMemo(() => buildSnapshot(cachedRoutes, visiblePathname), [cachedRoutes, visiblePathname]);
344
346
  useEffect(() => {
345
347
  if (isProduction()) return;
@@ -350,7 +352,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
350
352
  return lastSnapshotRef.current;
351
353
  };
352
354
  const api = {
353
- getSnapshot: () => lastSnapshotRef.current,
355
+ getSnapshot: () => lastSnapshotRef.current ?? snapshot,
354
356
  lastSnapshot: snapshot,
355
357
  refresh,
356
358
  setWarningThreshold: (nextThreshold) => {
@@ -368,10 +370,7 @@ function useRouterCacheDebug(cachedRoutes, visiblePathname) {
368
370
  warningThreshold: warningThresholdRef.current
369
371
  };
370
372
  const warningThreshold = warningThresholdRef.current;
371
- if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) {
372
- lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
373
- console.warn("[tanstack-router-cache] cached route count exceeded threshold", nextSnapshot);
374
- }
373
+ if (typeof warningThreshold === "number" && nextSnapshot.totalCachedRouteCount > warningThreshold && lastWarnedCountRef.current !== nextSnapshot.totalCachedRouteCount) lastWarnedCountRef.current = nextSnapshot.totalCachedRouteCount;
375
374
  });
376
375
  return () => {
377
376
  globalThis.cancelAnimationFrame(rafId);
@@ -622,6 +621,30 @@ function dismissTransientUi(container, pathname) {
622
621
  //#endregion
623
622
  //#region src/components/off-screen-in.tsx
624
623
  const windowScrollPositions = /* @__PURE__ */ new Map();
624
+ const IMMEDIATE_SCROLL_RESTORE_DELAY = 0;
625
+ const EARLY_SCROLL_RESTORE_DELAY = 80;
626
+ const MIDDLE_SCROLL_RESTORE_DELAY = 240;
627
+ const LATE_SCROLL_RESTORE_DELAY = 600;
628
+ const FINAL_SCROLL_RESTORE_DELAY = 1e3;
629
+ const SCROLL_TRACKING_INTERVAL_MS = 200;
630
+ const SCROLL_KEYS = new Set([
631
+ " ",
632
+ "ArrowDown",
633
+ "ArrowLeft",
634
+ "ArrowRight",
635
+ "ArrowUp",
636
+ "End",
637
+ "Home",
638
+ "PageDown",
639
+ "PageUp"
640
+ ]);
641
+ const SCROLL_RESTORE_DELAYS = [
642
+ IMMEDIATE_SCROLL_RESTORE_DELAY,
643
+ EARLY_SCROLL_RESTORE_DELAY,
644
+ MIDDLE_SCROLL_RESTORE_DELAY,
645
+ LATE_SCROLL_RESTORE_DELAY,
646
+ FINAL_SCROLL_RESTORE_DELAY
647
+ ];
625
648
  function OffScreenIn(props) {
626
649
  const { mode, children, containerRef, pathname } = props;
627
650
  const localContainerRef = useRef(null);
@@ -652,26 +675,61 @@ function OffScreenIn(props) {
652
675
  y: globalThis.scrollY
653
676
  });
654
677
  };
655
- if (mode === "hidden") {
656
- saveScrollPosition();
657
- return;
658
- }
678
+ if (mode === "hidden") return;
659
679
  const savedPosition = windowScrollPositions.get(pathname);
660
- const handleScroll = () => saveScrollPosition();
680
+ let userRequestedScroll = false;
681
+ let scrollTrackingIntervalId;
682
+ const startScrollTracking = () => {
683
+ scrollTrackingIntervalId ??= globalThis.setInterval(saveScrollPosition, SCROLL_TRACKING_INTERVAL_MS);
684
+ };
685
+ const handleScroll = () => {
686
+ if (!savedPosition || userRequestedScroll) saveScrollPosition();
687
+ };
688
+ const markUserRequestedScroll = () => {
689
+ userRequestedScroll = true;
690
+ };
691
+ const handleKeyDown = (event) => {
692
+ if (SCROLL_KEYS.has(event.key)) markUserRequestedScroll();
693
+ };
661
694
  globalThis.addEventListener("scroll", handleScroll, { passive: true });
695
+ globalThis.addEventListener("wheel", markUserRequestedScroll, { passive: true });
696
+ globalThis.addEventListener("touchstart", markUserRequestedScroll, { passive: true });
697
+ globalThis.addEventListener("pointerdown", markUserRequestedScroll, { passive: true });
698
+ globalThis.addEventListener("keydown", handleKeyDown);
662
699
  if (!savedPosition) {
700
+ startScrollTracking();
663
701
  saveScrollPosition();
664
- return () => globalThis.removeEventListener("scroll", handleScroll);
702
+ return () => {
703
+ if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
704
+ globalThis.removeEventListener("scroll", handleScroll);
705
+ globalThis.removeEventListener("wheel", markUserRequestedScroll);
706
+ globalThis.removeEventListener("touchstart", markUserRequestedScroll);
707
+ globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
708
+ globalThis.removeEventListener("keydown", handleKeyDown);
709
+ };
665
710
  }
711
+ const restoreScrollPosition = () => {
712
+ if (userRequestedScroll) return;
713
+ globalThis.scrollTo(savedPosition.x, savedPosition.y);
714
+ saveScrollPosition();
715
+ startScrollTracking();
716
+ };
717
+ const timeoutIds = [];
666
718
  let rafId = globalThis.requestAnimationFrame(() => {
667
719
  rafId = globalThis.requestAnimationFrame(() => {
668
- globalThis.scrollTo(savedPosition.x, savedPosition.y);
669
- saveScrollPosition();
720
+ restoreScrollPosition();
721
+ for (const delay of SCROLL_RESTORE_DELAYS) timeoutIds.push(globalThis.setTimeout(restoreScrollPosition, delay));
670
722
  });
671
723
  });
672
724
  return () => {
673
725
  globalThis.cancelAnimationFrame(rafId);
726
+ for (const timeoutId of timeoutIds) globalThis.clearTimeout(timeoutId);
727
+ if (scrollTrackingIntervalId !== void 0) globalThis.clearInterval(scrollTrackingIntervalId);
674
728
  globalThis.removeEventListener("scroll", handleScroll);
729
+ globalThis.removeEventListener("wheel", markUserRequestedScroll);
730
+ globalThis.removeEventListener("touchstart", markUserRequestedScroll);
731
+ globalThis.removeEventListener("pointerdown", markUserRequestedScroll);
732
+ globalThis.removeEventListener("keydown", handleKeyDown);
675
733
  };
676
734
  }, [mode, pathname]);
677
735
  return /* @__PURE__ */ jsx(Activity, {
@@ -741,15 +799,8 @@ function getLiveRouterMethodDescriptors(router) {
741
799
  return [[methodName, { value: method.bind(router) }]];
742
800
  }));
743
801
  }
744
- function isRouterSearch(value) {
745
- return typeof value === "object" && value !== null;
746
- }
747
802
  function toRouterLocation(location) {
748
- return {
749
- href: location.href,
750
- pathname: location.pathname,
751
- search: isRouterSearch(location.search) ? location.search : void 0
752
- };
803
+ return { ...location };
753
804
  }
754
805
  function isReadyCachedRoute(route) {
755
806
  return Boolean(route && isRouteCacheEnabled(route.staticData) && route.ready && route.matchId && route.routerSnapshot);
@@ -961,7 +1012,8 @@ function RouteCacheManager() {
961
1012
  const pendingCachedNavigationRef = useRef(null);
962
1013
  const previousPathnameRef = useRef(void 0);
963
1014
  const previousHrefRef = useRef(void 0);
964
- const previousRouteCacheModesRef = useRef(/* @__PURE__ */ new Map());
1015
+ const previousRouteCacheModesRef = useRef(null);
1016
+ if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
965
1017
  const previousVisiblePathnameRef = useRef(void 0);
966
1018
  const routerLocation = useRouterState({ select: (state) => toRouterLocation(state.location) });
967
1019
  const routerHref = routerLocation.href;
@@ -1057,6 +1109,7 @@ function RouteCacheManager() {
1057
1109
  visiblePathname
1058
1110
  ]);
1059
1111
  useLayoutEffect(() => {
1112
+ if (previousRouteCacheModesRef.current === null) previousRouteCacheModesRef.current = /* @__PURE__ */ new Map();
1060
1113
  previousRouteCacheModesRef.current = syncCachedRouteActivityEvents({
1061
1114
  cachedRoutes,
1062
1115
  eventListener,
package/docs/releases.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Releases
2
2
 
3
- This package publishes from GitHub Actions after a version tag is pushed.
3
+ This package publishes from GitHub Actions after a version tag is pushed. A regular commit to `main` only runs CI and updates the GitHub repo. It does not publish to npm.
4
4
 
5
5
  ## Trusted publishing
6
6
 
@@ -14,13 +14,37 @@ npm trust github tanstack-router-cache --repo santiago-ramos-02/tanstack-router-
14
14
 
15
15
  ## Publishing a version
16
16
 
17
- 1. Update `version` in `package.json`.
18
- 2. Commit the change.
19
- 3. Tag the same version:
17
+ Work in the package repo:
20
18
 
21
19
  ```sh
22
- git tag v0.2.0
23
- git push origin main --tags
20
+ cd packages/tanstack-router-cache
24
21
  ```
25
22
 
26
- The workflow checks that the tag matches `package.json` before publishing.
23
+ For normal changes:
24
+
25
+ ```sh
26
+ bun run check
27
+ git add .
28
+ git commit -m "Your change"
29
+ git push origin main
30
+ ```
31
+
32
+ That updates GitHub and runs CI, but it does not publish to npm.
33
+
34
+ For a release, start from a clean `main` branch after your changes are already committed:
35
+
36
+ ```sh
37
+ bun run release:prepare patch
38
+ bun run release:push
39
+ ```
40
+
41
+ Use `minor`, `major`, or an exact version when needed:
42
+
43
+ ```sh
44
+ bun run release:prepare minor
45
+ bun run release:prepare 0.2.0
46
+ ```
47
+
48
+ `release:prepare` updates `package.json`, runs the package checks, commits the version bump, and creates the matching tag locally. `release:push` pushes `main` and that exact tag. The tag starts the npm publish workflow.
49
+
50
+ The workflow checks that the tag matches `package.json` before publishing, so a mismatched tag cannot publish accidentally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanstack-router-cache",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Route view caching for TanStack Router.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,9 +46,12 @@
46
46
  "build": "bun run clean && rolldown -c && tsc",
47
47
  "typecheck": "tsc --noEmit",
48
48
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
49
- "lint": "biome check .",
50
- "lint:fix": "biome check . --write",
51
- "pack:dry-run": "bun pm pack --dry-run",
49
+ "lint": "biome lint src scripts .github/scripts rolldown.config.ts --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
50
+ "lint:fix": "biome lint src scripts .github/scripts rolldown.config.ts --write --skip=lint/performance/noBarrelFile --skip=lint/style/useConsistentTypeDefinitions --skip=lint/suspicious/noConsole",
51
+ "check": "bun run lint && bun run typecheck && bun run build && bun run pack:dry-run",
52
+ "pack:dry-run": "npm pack --dry-run",
53
+ "release:prepare": "node scripts/release.mjs prepare",
54
+ "release:push": "node scripts/release.mjs push",
52
55
  "prepack": "bun run build",
53
56
  "prepublishOnly": "bun run lint && bun run typecheck"
54
57
  },