cursedbelt 5.21.0 → 5.22.1

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.
@@ -30,7 +30,9 @@ import { type RefObject } from 'react';
30
30
  * - **A replaced URL is the same place**: when the location changes but the history entry does
31
31
  * not (a filter written with `replaceState`), the scroll is left alone.
32
32
  *
33
- * Mount it ONCE per scroller, above the screens that swap — the app shell, keyed on the route.
33
+ * Mount it ONCE per scroller: in the app shell keyed on the route when the screens swap under one
34
+ * scroller (the window, a shell `ScrollArea`), or in the list screen itself when each screen owns
35
+ * its own `ScrollArea` — mounting is then the arrival and unmounting the leave.
34
36
  */
35
37
  export declare function useScrollMemory(location: string, options?: ScrollMemoryOptions): void;
36
38
  /** What scrolls: the window, or an element — by ref or by a getter read when the screen arrives. */
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollMemory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useScrollMemory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA2B,MAAM,OAAO,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI,CA2DzF;AAED,oGAAoG;AACpG,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"useScrollMemory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useScrollMemory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA2B,MAAM,OAAO,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI,CA0BzF;AAoED,oGAAoG;AACpG,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
@@ -30,68 +30,87 @@ import { useLayoutEffect, useRef } from 'react';
30
30
  * - **A replaced URL is the same place**: when the location changes but the history entry does
31
31
  * not (a filter written with `replaceState`), the scroll is left alone.
32
32
  *
33
- * Mount it ONCE per scroller, above the screens that swap — the app shell, keyed on the route.
33
+ * Mount it ONCE per scroller: in the app shell keyed on the route when the screens swap under one
34
+ * scroller (the window, a shell `ScrollArea`), or in the list screen itself when each screen owns
35
+ * its own `ScrollArea` — mounting is then the arrival and unmounting the leave.
34
36
  */
35
37
  export function useScrollMemory(location, options = {}) {
36
38
  const { enabled = true, deadlineMs = RESTORE_DEADLINE_MS } = options;
37
- // The scroller is read through a ref so an inline `() => el` does not re-run the effect
38
- // every render — which would file and restore on every keystroke.
39
- const scrollerRef = useRef(options.scroller);
40
- scrollerRef.current = options.scroller;
39
+ const spec = options.scroller ?? 'window';
41
40
  const last = useRef(null);
41
+ const session = useRef(null);
42
+ // 🔴 Checked EVERY commit, not on a dependency list: the scroller can mount after the hook — a
43
+ // list behind a loading spinner has no ScrollArea on its first render — and an effect keyed on
44
+ // the location alone would find nothing, return, and never look again, so that whole visit
45
+ // would go unremembered. A session ends and the next begins only when the place, the switch or
46
+ // the scroller ELEMENT actually changes; every other commit is one comparison.
42
47
  useLayoutEffect(() => {
43
- if (!enabled)
48
+ const scroller = enabled ? resolveScroller(spec) : null;
49
+ const current = session.current;
50
+ if (current && scroller && current.location === location && current.target === scroller.events)
44
51
  return;
45
- const scroller = resolveScroller(scrollerRef.current ?? 'window');
46
- if (!scroller)
52
+ if (!current && !scroller)
47
53
  return;
48
- if (scroller.isWindow && 'scrollRestoration' in window.history) {
49
- // Left on `auto`, the browser restores at `popstate` — into the screen being LEFT, before
50
- // React has swapped it — and the restore below would have to fight it.
51
- window.history.scrollRestoration = 'manual';
54
+ current?.end();
55
+ session.current = scroller ? begin(scroller, location, last, deadlineMs) : null;
56
+ });
57
+ useLayoutEffect(() => () => {
58
+ session.current?.end();
59
+ session.current = null;
60
+ }, []);
61
+ }
62
+ /** Arrive on the current history entry with this scroller: restore or start at the top, then track. */
63
+ function begin(scroller, location, last, deadlineMs) {
64
+ if (scroller.isWindow && 'scrollRestoration' in window.history) {
65
+ // Left on `auto`, the browser restores at `popstate` — into the screen being LEFT, before
66
+ // React has swapped it — and the restore below would have to fight it.
67
+ window.history.scrollRestoration = 'manual';
68
+ }
69
+ const id = entryId();
70
+ if (!id)
71
+ return null;
72
+ // Same entry, different location: the URL was replaced in place. It is the same place.
73
+ const replacedInPlace = last.current?.id === id && last.current.location !== location;
74
+ last.current = { id, location };
75
+ // What gets filed when this entry is left. While a restore is pending it is the TARGET, not
76
+ // the partial offset on screen — leaving mid-restore (or StrictMode's rehearsal unmount) must
77
+ // not overwrite a deep position with a shallow one.
78
+ let offset = scroller.get();
79
+ let cancelRestore = null;
80
+ if (!replacedInPlace) {
81
+ const target = recalled(id);
82
+ if (target > 0) {
83
+ offset = target;
84
+ cancelRestore = restore(scroller, target, deadlineMs, (outcome) => {
85
+ cancelRestore = null;
86
+ if (outcome !== 'reached')
87
+ offset = scroller.get();
88
+ });
52
89
  }
53
- const id = entryId();
54
- if (!id)
55
- return;
56
- // Same entry, different location: the URL was replaced in place. It is the same place.
57
- const replacedInPlace = last.current?.id === id && last.current.location !== location;
58
- last.current = { id, location };
59
- // What gets filed when this entry is left. While a restore is pending it is the TARGET, not
60
- // the partial offset on screen — leaving mid-restore (or StrictMode's rehearsal unmount) must
61
- // not overwrite a deep position with a shallow one.
62
- let offset = scroller.get();
63
- let cancelRestore = null;
64
- if (!replacedInPlace) {
65
- const target = recalled(id);
66
- if (target > 0) {
67
- offset = target;
68
- cancelRestore = restore(scroller, target, deadlineMs, (outcome) => {
69
- cancelRestore = null;
70
- if (outcome !== 'reached')
71
- offset = scroller.get();
72
- });
73
- }
74
- else {
75
- scroller.set(0);
76
- offset = 0;
77
- }
90
+ else {
91
+ scroller.set(0);
92
+ offset = 0;
78
93
  }
79
- const onScroll = () => {
80
- if (!cancelRestore)
81
- offset = scroller.get();
82
- };
83
- const onHide = () => file(id, offset);
84
- scroller.events.addEventListener('scroll', onScroll, { passive: true });
85
- window.addEventListener('pagehide', onHide);
86
- return () => {
94
+ }
95
+ const onScroll = () => {
96
+ if (!cancelRestore)
97
+ offset = scroller.get();
98
+ };
99
+ const onHide = () => file(id, offset);
100
+ scroller.events.addEventListener('scroll', onScroll, { passive: true });
101
+ window.addEventListener('pagehide', onHide);
102
+ return {
103
+ location,
104
+ target: scroller.events,
105
+ end() {
87
106
  const pending = cancelRestore;
88
107
  cancelRestore = null;
89
108
  pending?.();
90
109
  scroller.events.removeEventListener('scroll', onScroll);
91
110
  window.removeEventListener('pagehide', onHide);
92
111
  file(id, offset);
93
- };
94
- }, [location, enabled, deadlineMs]);
112
+ },
113
+ };
95
114
  }
96
115
  const RESTORE_DEADLINE_MS = 3_000;
97
116
  const ENTRY_KEY = 'cbScrollEntry';
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollMemory.js","sourceRoot":"","sources":["../../../src/react/hooks/useScrollMemory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useScrollMemory.js","sourceRoot":"","sources":["../../../src/react/hooks/useScrollMemory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,UAA+B,EAAE;IACjF,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,UAAU,GAAG,mBAAmB,EAAE,GAAG,OAAO,CAAC;IACrE,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAC1C,MAAM,IAAI,GAAG,MAAM,CAA0C,IAAI,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAE7C,+FAA+F;IAC/F,+FAA+F;IAC/F,2FAA2F;IAC3F,+FAA+F;IAC/F,+EAA+E;IAC/E,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACxD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,OAAO,IAAI,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAAE,OAAO;QACvG,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ;YAAE,OAAO;QAClC,OAAO,EAAE,GAAG,EAAE,CAAC;QACf,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClF,CAAC,CAAC,CAAC;IACH,eAAe,CACb,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;QACvB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IACzB,CAAC,EACD,EAAE,CACH,CAAC;AACJ,CAAC;AASD,uGAAuG;AACvG,SAAS,KAAK,CACZ,QAAkB,EAClB,QAAgB,EAChB,IAA0D,EAC1D,UAAkB;IAElB,IAAI,QAAQ,CAAC,QAAQ,IAAI,mBAAmB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/D,0FAA0F;QAC1F,uEAAuE;QACvE,MAAM,CAAC,OAAO,CAAC,iBAAiB,GAAG,QAAQ,CAAC;IAC9C,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAErB,uFAAuF;IACvF,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACtF,IAAI,CAAC,OAAO,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;IAEhC,4FAA4F;IAC5F,8FAA8F;IAC9F,oDAAoD;IACpD,IAAI,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;IAC5B,IAAI,aAAa,GAAwB,IAAI,CAAC;IAE9C,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;YACf,MAAM,GAAG,MAAM,CAAC;YAChB,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE;gBAChE,aAAa,GAAG,IAAI,CAAC;gBACrB,IAAI,OAAO,KAAK,SAAS;oBAAE,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrD,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM,GAAG,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,CAAC,aAAa;YAAE,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;IAC9C,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC5C,OAAO;QACL,QAAQ;QACR,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,GAAG;YACD,MAAM,OAAO,GAAG,aAAa,CAAC;YAC9B,aAAa,GAAG,IAAI,CAAC;YACrB,OAAO,EAAE,EAAE,CAAC;YACZ,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACxD,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;YAC/C,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAsBD,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAClC,MAAM,SAAS,GAAG,eAAe,CAAC;AAClC,MAAM,SAAS,GAAG,0BAA0B,CAAC;AAC7C,4FAA4F;AAC5F,MAAM,IAAI,GAAG,GAAG,CAAC;AAEjB,sGAAsG;AACtG,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,CAAU,CAAC;AAWhF,SAAS,eAAe,CAAC,IAA0B;IACjD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO;YACL,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;YACrC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;YACjC,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY,GAAG,MAAM,CAAC,WAAW;SACvE,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IAC9D,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC;QACnC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE;YACT,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,YAAY,GAAG,EAAE,CAAC,YAAY;KAC9C,CAAC;AACJ,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAgC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC;AAEnG,gGAAgG;AAChG,SAAS,OAAO;IACd,MAAM,KAAK,GAAY,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;IAC5C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,sBAAsB;IAClG,MAAM,QAAQ,GAAG,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAClD,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAClF,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEzC,SAAS,MAAM;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,CAAC;QACrF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,MAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU;IAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IACzC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,IAAI,CAAC,EAAU,EAAE,CAAS;IACjC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;QACrB,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,kDAAkD;QAClE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;QAChF,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,wFAAwF;IAC1F,CAAC;AACH,CAAC;AAID,2FAA2F;AAC3F,SAAS,OAAO,CACd,QAAkB,EAClB,MAAc,EACd,UAAkB,EAClB,OAA0C;IAE1C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC;IAChD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,IAAI,GAAG,KAAK,CAAC;IAEjB,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QACvB,IAAI,GAAG,IAAI,CAAC;QACZ,oBAAoB,CAAC,KAAK,CAAC,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,YAAY;YAAE,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,IAAI,EAAE;YAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,IAAI;YAAE,OAAO;QACjB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACnB,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrB,IAAI,IAAI,EAAE;gBAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YAC/B,OAAO;QACT,CAAC;QACD,2FAA2F;QAC3F,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAChC,IAAI,WAAW,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;YAClC,IAAI,IAAI,EAAE;gBAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QAClC,CAAC;;YAAM,KAAK,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,YAAY;QAAE,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3F,OAAO,EAAE,CAAC;IACV,OAAO,GAAG,EAAE;QACV,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -22,9 +22,22 @@ export interface UsePagedListOptions<T> {
22
22
  pollWhile?: (items: T[]) => boolean;
23
23
  /** ms a page stays fresh before a background refetch (TanStack `staleTime`). */
24
24
  staleTime?: number;
25
+ /**
26
+ * ms an unwatched list's pages stay cached (TanStack `gcTime`, default 5 minutes). Raise it
27
+ * when the trip away is long — a film watched from a grid is two hours, and Back should still
28
+ * find every page.
29
+ */
30
+ gcTime?: number;
31
+ /**
32
+ * Keep the previous query's items on screen while a changed `queryKey` fetches its first
33
+ * page, instead of emptying the list — a search typed a letter at a time does not flash
34
+ * blank between keystrokes. `isLoading` stays false meanwhile; `isFetching` is true.
35
+ */
36
+ keepPreviousItems?: boolean;
25
37
  }
26
38
  export interface PagedList<T> {
27
- /** Every loaded page, flattened in order. */
39
+ /** Every loaded page, flattened in order — the same array until a page changes, so it is
40
+ * safe as a `useMemo` dependency. */
28
41
  items: T[];
29
42
  /** First load only (no pages yet) — drive skeletons off this, not `isFetching`. */
30
43
  isLoading: boolean;
@@ -54,5 +67,5 @@ export interface PagedList<T> {
54
67
  * 30,000px down instead of at the end of page one. Give it a `staleTime` too, or the return
55
68
  * trip refetches every page it holds in the background.
56
69
  */
57
- export declare function usePagedList<T>({ queryKey, fetchPage, enabled, pollIntervalMs, pollWhile, staleTime, }: UsePagedListOptions<T>): PagedList<T>;
70
+ export declare function usePagedList<T>({ queryKey, fetchPage, enabled, pollIntervalMs, pollWhile, staleTime, gcTime, keepPreviousItems, }: UsePagedListOptions<T>): PagedList<T>;
58
71
  //# sourceMappingURL=usePagedList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePagedList.d.ts","sourceRoot":"","sources":["../../src/react/usePagedList.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAoB,MAAM,uBAAuB,CAAC;AAgBxE,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC;wFACoF;IACpF,QAAQ,EAAE,QAAQ,CAAC;IACnB,kEAAkE;IAClE,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;mFAC+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;kBACc;IACd,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC;IACpC,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,6CAA6C;IAC7C,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,mFAAmF;IACnF,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,gGAAgG;IAChG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,kEAAkE;IAClE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAC9B,QAAQ,EACR,SAAS,EACT,OAAc,EACd,cAAc,EACd,SAAS,EACT,SAAS,GACV,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAsCvC"}
1
+ {"version":3,"file":"usePagedList.d.ts","sourceRoot":"","sources":["../../src/react/usePagedList.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,QAAQ,EAAoB,MAAM,uBAAuB,CAAC;AAiB1F,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC;wFACoF;IACpF,QAAQ,EAAE,QAAQ,CAAC;IACnB,kEAAkE;IAClE,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;mFAC+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;kBACc;IACd,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC;IACpC,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B;0CACsC;IACtC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,mFAAmF;IACnF,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,iCAAiC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,gGAAgG;IAChG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,kEAAkE;IAClE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAC9B,QAAQ,EACR,SAAS,EACT,OAAc,EACd,cAAc,EACd,SAAS,EACT,SAAS,EACT,MAAM,EACN,iBAAyB,GAC1B,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CA2CvC"}
@@ -1,4 +1,5 @@
1
- import { useInfiniteQuery } from '@tanstack/react-query';
1
+ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
2
+ import { useMemo } from 'react';
2
3
  /**
3
4
  * Cursor-paged list state as one hook. Point it at a `fetchPage(cursor)` and give
4
5
  * it a `queryKey` that encodes the active filters; it owns paging, the
@@ -12,11 +13,13 @@ import { useInfiniteQuery } from '@tanstack/react-query';
12
13
  * 30,000px down instead of at the end of page one. Give it a `staleTime` too, or the return
13
14
  * trip refetches every page it holds in the background.
14
15
  */
15
- export function usePagedList({ queryKey, fetchPage, enabled = true, pollIntervalMs, pollWhile, staleTime, }) {
16
+ export function usePagedList({ queryKey, fetchPage, enabled = true, pollIntervalMs, pollWhile, staleTime, gcTime, keepPreviousItems = false, }) {
16
17
  const query = useInfiniteQuery({
17
18
  queryKey,
18
19
  enabled,
19
20
  staleTime,
21
+ gcTime,
22
+ placeholderData: keepPreviousItems ? keepPreviousData : undefined,
20
23
  initialPageParam: undefined,
21
24
  queryFn: ({ pageParam }) => fetchPage(pageParam),
22
25
  getNextPageParam: (last) => last.nextCursor,
@@ -30,8 +33,11 @@ export function usePagedList({ queryKey, fetchPage, enabled = true, pollInterval
30
33
  }
31
34
  : false,
32
35
  });
33
- const pages = query.data?.pages ?? [];
34
- const items = pages.flatMap((p) => p.items);
36
+ const pages = query.data?.pages;
37
+ // Memoized on the query's data, which TanStack keeps referentially stable while nothing
38
+ // changed — flattening per render handed every consumer a new array and defeated its memos.
39
+ const items = useMemo(() => (pages ?? []).flatMap((p) => p.items), [pages]);
40
+ const total = useMemo(() => pages?.findLast((p) => p.total !== undefined)?.total, [pages]);
35
41
  return {
36
42
  items,
37
43
  isLoading: query.isLoading,
@@ -39,7 +45,7 @@ export function usePagedList({ queryKey, fetchPage, enabled = true, pollInterval
39
45
  isError: query.isError,
40
46
  error: query.error,
41
47
  hasMore: !!query.hasNextPage,
42
- total: pages.findLast((p) => p.total !== undefined)?.total,
48
+ total,
43
49
  loadMore: () => {
44
50
  if (query.hasNextPage && !query.isFetchingNextPage)
45
51
  void query.fetchNextPage();
@@ -1 +1 @@
1
- {"version":3,"file":"usePagedList.js","sourceRoot":"","sources":["../../src/react/usePagedList.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AA6DxE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAI,EAC9B,QAAQ,EACR,SAAS,EACT,OAAO,GAAG,IAAI,EACd,cAAc,EACd,SAAS,EACT,SAAS,GACc;IACvB,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,QAAQ;QACR,OAAO;QACP,SAAS;QACT,gBAAgB,EAAE,SAA+B;QACjD,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC;QAChD,gBAAgB,EAAE,CAAC,IAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU;QACzD,6EAA6E;QAC7E,6DAA6D;QAC7D,eAAe,EACb,cAAc,IAAI,SAAS;YACzB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;gBACJ,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAmC,CAAC;gBAChE,MAAM,KAAK,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;YACnD,CAAC;YACH,CAAC,CAAC,KAAK;KACZ,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAE5C,OAAO;QACL,KAAK;QACL,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW;QAC5B,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,KAAK;QAC1D,QAAQ,EAAE,GAAG,EAAE;YACb,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,kBAAkB;gBAAE,KAAK,KAAK,CAAC,aAAa,EAAE,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"usePagedList.js","sourceRoot":"","sources":["../../src/react/usePagedList.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAiB,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AA0EhC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAI,EAC9B,QAAQ,EACR,SAAS,EACT,OAAO,GAAG,IAAI,EACd,cAAc,EACd,SAAS,EACT,SAAS,EACT,MAAM,EACN,iBAAiB,GAAG,KAAK,GACF;IACvB,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,QAAQ;QACR,OAAO;QACP,SAAS;QACT,MAAM;QACN,eAAe,EAAE,iBAAiB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS;QACjE,gBAAgB,EAAE,SAA+B;QACjD,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC;QAChD,gBAAgB,EAAE,CAAC,IAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU;QACzD,6EAA6E;QAC7E,6DAA6D;QAC7D,eAAe,EACb,cAAc,IAAI,SAAS;YACzB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;gBACJ,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAmC,CAAC;gBAChE,MAAM,KAAK,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;YACnD,CAAC;YACH,CAAC,CAAC,KAAK;KACZ,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;IAChC,wFAAwF;IACxF,4FAA4F;IAC5F,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAE3F,OAAO;QACL,KAAK;QACL,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW;QAC5B,KAAK;QACL,QAAQ,EAAE,GAAG,EAAE;YACb,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,kBAAkB;gBAAE,KAAK,KAAK,CAAC,aAAa,EAAE,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.21.0",
3
+ "version": "5.22.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -224,4 +224,42 @@ describe('useScrollMemory', () => {
224
224
  render(<Shell at='/' el={s.el} />);
225
225
  expect(s.el.scrollTop).toBe(12_000);
226
226
  });
227
+
228
+ test('a scroller that mounts AFTER the hook — a list behind a loading spinner — is still remembered', () => {
229
+ const s = fakeScroller(20_000);
230
+ const { rerender } = render(<Shell at='/' el={s.el} />);
231
+ s.scrollTo(8_000);
232
+ const list = { state: window.history.state, path: '/' };
233
+ push('/item/7');
234
+ rerender(<Shell at='/item/7' el={s.el} />);
235
+
236
+ back(list);
237
+ const Late = ({ ready }: { ready: boolean }) => {
238
+ useScrollMemory('/', { scroller: () => (ready ? s.el : null) });
239
+ return null;
240
+ };
241
+ cleanup();
242
+ const late = render(<Late ready={false} />);
243
+ late.rerender(<Late ready />);
244
+ expect(s.el.scrollTop).toBe(8_000);
245
+ });
246
+
247
+ test('a list screen that owns its scroller files on unmount and restores on remount', () => {
248
+ const s = fakeScroller(20_000);
249
+ const List = () => {
250
+ useScrollMemory('/people', { scroller: () => s.el });
251
+ return null;
252
+ };
253
+ window.history.replaceState(null, '', '/people');
254
+ const first = render(<List />);
255
+ s.scrollTo(5_500);
256
+ const list = { state: window.history.state, path: '/people' };
257
+ push('/person/1'); // the app navigates; the list unmounts
258
+ first.unmount();
259
+ s.el.scrollTop = 0; // a new screen's scroller, as far as this list knows
260
+
261
+ back(list);
262
+ render(<List />);
263
+ expect(s.el.scrollTop).toBe(5_500);
264
+ });
227
265
  });
@@ -31,67 +31,102 @@ import { type RefObject, useLayoutEffect, useRef } from 'react';
31
31
  * - **A replaced URL is the same place**: when the location changes but the history entry does
32
32
  * not (a filter written with `replaceState`), the scroll is left alone.
33
33
  *
34
- * Mount it ONCE per scroller, above the screens that swap — the app shell, keyed on the route.
34
+ * Mount it ONCE per scroller: in the app shell keyed on the route when the screens swap under one
35
+ * scroller (the window, a shell `ScrollArea`), or in the list screen itself when each screen owns
36
+ * its own `ScrollArea` — mounting is then the arrival and unmounting the leave.
35
37
  */
36
38
  export function useScrollMemory(location: string, options: ScrollMemoryOptions = {}): void {
37
39
  const { enabled = true, deadlineMs = RESTORE_DEADLINE_MS } = options;
38
- // The scroller is read through a ref so an inline `() => el` does not re-run the effect
39
- // every render — which would file and restore on every keystroke.
40
- const scrollerRef = useRef(options.scroller);
41
- scrollerRef.current = options.scroller;
40
+ const spec = options.scroller ?? 'window';
42
41
  const last = useRef<{ id: string; location: string } | null>(null);
42
+ const session = useRef<Session | null>(null);
43
43
 
44
+ // 🔴 Checked EVERY commit, not on a dependency list: the scroller can mount after the hook — a
45
+ // list behind a loading spinner has no ScrollArea on its first render — and an effect keyed on
46
+ // the location alone would find nothing, return, and never look again, so that whole visit
47
+ // would go unremembered. A session ends and the next begins only when the place, the switch or
48
+ // the scroller ELEMENT actually changes; every other commit is one comparison.
44
49
  useLayoutEffect(() => {
45
- if (!enabled) return;
46
- const scroller = resolveScroller(scrollerRef.current ?? 'window');
47
- if (!scroller) return;
48
- if (scroller.isWindow && 'scrollRestoration' in window.history) {
49
- // Left on `auto`, the browser restores at `popstate` — into the screen being LEFT, before
50
- // React has swapped it — and the restore below would have to fight it.
51
- window.history.scrollRestoration = 'manual';
52
- }
53
- const id = entryId();
54
- if (!id) return;
55
-
56
- // Same entry, different location: the URL was replaced in place. It is the same place.
57
- const replacedInPlace = last.current?.id === id && last.current.location !== location;
58
- last.current = { id, location };
59
-
60
- // What gets filed when this entry is left. While a restore is pending it is the TARGET, not
61
- // the partial offset on screen — leaving mid-restore (or StrictMode's rehearsal unmount) must
62
- // not overwrite a deep position with a shallow one.
63
- let offset = scroller.get();
64
- let cancelRestore: (() => void) | null = null;
65
-
66
- if (!replacedInPlace) {
67
- const target = recalled(id);
68
- if (target > 0) {
69
- offset = target;
70
- cancelRestore = restore(scroller, target, deadlineMs, (outcome) => {
71
- cancelRestore = null;
72
- if (outcome !== 'reached') offset = scroller.get();
73
- });
74
- } else {
75
- scroller.set(0);
76
- offset = 0;
77
- }
50
+ const scroller = enabled ? resolveScroller(spec) : null;
51
+ const current = session.current;
52
+ if (current && scroller && current.location === location && current.target === scroller.events) return;
53
+ if (!current && !scroller) return;
54
+ current?.end();
55
+ session.current = scroller ? begin(scroller, location, last, deadlineMs) : null;
56
+ });
57
+ useLayoutEffect(
58
+ () => () => {
59
+ session.current?.end();
60
+ session.current = null;
61
+ },
62
+ [],
63
+ );
64
+ }
65
+
66
+ interface Session {
67
+ location: string;
68
+ target: EventTarget;
69
+ /** Stop tracking and file the offset under this entry. */
70
+ end(): void;
71
+ }
72
+
73
+ /** Arrive on the current history entry with this scroller: restore or start at the top, then track. */
74
+ function begin(
75
+ scroller: Scroller,
76
+ location: string,
77
+ last: { current: { id: string; location: string } | null },
78
+ deadlineMs: number,
79
+ ): Session | null {
80
+ if (scroller.isWindow && 'scrollRestoration' in window.history) {
81
+ // Left on `auto`, the browser restores at `popstate` — into the screen being LEFT, before
82
+ // React has swapped it — and the restore below would have to fight it.
83
+ window.history.scrollRestoration = 'manual';
84
+ }
85
+ const id = entryId();
86
+ if (!id) return null;
87
+
88
+ // Same entry, different location: the URL was replaced in place. It is the same place.
89
+ const replacedInPlace = last.current?.id === id && last.current.location !== location;
90
+ last.current = { id, location };
91
+
92
+ // What gets filed when this entry is left. While a restore is pending it is the TARGET, not
93
+ // the partial offset on screen — leaving mid-restore (or StrictMode's rehearsal unmount) must
94
+ // not overwrite a deep position with a shallow one.
95
+ let offset = scroller.get();
96
+ let cancelRestore: (() => void) | null = null;
97
+
98
+ if (!replacedInPlace) {
99
+ const target = recalled(id);
100
+ if (target > 0) {
101
+ offset = target;
102
+ cancelRestore = restore(scroller, target, deadlineMs, (outcome) => {
103
+ cancelRestore = null;
104
+ if (outcome !== 'reached') offset = scroller.get();
105
+ });
106
+ } else {
107
+ scroller.set(0);
108
+ offset = 0;
78
109
  }
110
+ }
79
111
 
80
- const onScroll = () => {
81
- if (!cancelRestore) offset = scroller.get();
82
- };
83
- const onHide = () => file(id, offset);
84
- scroller.events.addEventListener('scroll', onScroll, { passive: true });
85
- window.addEventListener('pagehide', onHide);
86
- return () => {
112
+ const onScroll = () => {
113
+ if (!cancelRestore) offset = scroller.get();
114
+ };
115
+ const onHide = () => file(id, offset);
116
+ scroller.events.addEventListener('scroll', onScroll, { passive: true });
117
+ window.addEventListener('pagehide', onHide);
118
+ return {
119
+ location,
120
+ target: scroller.events,
121
+ end() {
87
122
  const pending = cancelRestore;
88
123
  cancelRestore = null;
89
124
  pending?.();
90
125
  scroller.events.removeEventListener('scroll', onScroll);
91
126
  window.removeEventListener('pagehide', onHide);
92
127
  file(id, offset);
93
- };
94
- }, [location, enabled, deadlineMs]);
128
+ },
129
+ };
95
130
  }
96
131
 
97
132
  /** What scrolls: the window, or an element — by ref or by a getter read when the screen arrives. */
@@ -99,4 +99,37 @@ describe('usePagedList', () => {
99
99
  expect(seen[0]).toBe(4);
100
100
  expect(fetches).toBe(2);
101
101
  });
102
+
103
+ test('items is the same array across renders until a page changes', async () => {
104
+ const fetchPage = pagedFetcher({ '@first': { items: ['a', 'b'] } });
105
+ const { result, rerender } = renderHook(() => usePagedList({ queryKey: ['stable'], fetchPage }), {
106
+ wrapper: makeWrapper(),
107
+ });
108
+ await waitFor(() => expect(result.current.items).toHaveLength(2));
109
+ const before = result.current.items;
110
+ rerender();
111
+ expect(result.current.items).toBe(before);
112
+ });
113
+
114
+ test('keepPreviousItems holds the old list on screen while a new query loads', async () => {
115
+ let release: () => void = () => {};
116
+ const fetchPage = (key: string) => (cursor: string | undefined) =>
117
+ key === 'b'
118
+ ? new Promise<PagedPage<string>>((r) => {
119
+ release = () => r({ items: ['b1'] });
120
+ })
121
+ : pagedFetcher({ '@first': { items: ['a1', 'a2'] } })(cursor);
122
+ const { result, rerender } = renderHook(
123
+ ({ q }: { q: string }) =>
124
+ usePagedList({ queryKey: ['keep', q], fetchPage: fetchPage(q), keepPreviousItems: true }),
125
+ { wrapper: makeWrapper(), initialProps: { q: 'a' } },
126
+ );
127
+ await waitFor(() => expect(result.current.items).toEqual(['a1', 'a2']));
128
+ rerender({ q: 'b' });
129
+ await waitFor(() => expect(result.current.isFetching).toBe(true));
130
+ expect(result.current.items).toEqual(['a1', 'a2']);
131
+ expect(result.current.isLoading).toBe(false);
132
+ act(() => release());
133
+ await waitFor(() => expect(result.current.items).toEqual(['b1']));
134
+ });
102
135
  });
@@ -1,4 +1,5 @@
1
- import { type QueryKey, useInfiniteQuery } from '@tanstack/react-query';
1
+ import { keepPreviousData, type QueryKey, useInfiniteQuery } from '@tanstack/react-query';
2
+ import { useMemo } from 'react';
2
3
 
3
4
  // ── usePagedList — one cursor-paged list layer over TanStack ─────────────────
4
5
  // Every "cursor → items → nextCursor, append on Load more" list re-rolled the
@@ -38,10 +39,23 @@ export interface UsePagedListOptions<T> {
38
39
  pollWhile?: (items: T[]) => boolean;
39
40
  /** ms a page stays fresh before a background refetch (TanStack `staleTime`). */
40
41
  staleTime?: number;
42
+ /**
43
+ * ms an unwatched list's pages stay cached (TanStack `gcTime`, default 5 minutes). Raise it
44
+ * when the trip away is long — a film watched from a grid is two hours, and Back should still
45
+ * find every page.
46
+ */
47
+ gcTime?: number;
48
+ /**
49
+ * Keep the previous query's items on screen while a changed `queryKey` fetches its first
50
+ * page, instead of emptying the list — a search typed a letter at a time does not flash
51
+ * blank between keystrokes. `isLoading` stays false meanwhile; `isFetching` is true.
52
+ */
53
+ keepPreviousItems?: boolean;
41
54
  }
42
55
 
43
56
  export interface PagedList<T> {
44
- /** Every loaded page, flattened in order. */
57
+ /** Every loaded page, flattened in order — the same array until a page changes, so it is
58
+ * safe as a `useMemo` dependency. */
45
59
  items: T[];
46
60
  /** First load only (no pages yet) — drive skeletons off this, not `isFetching`. */
47
61
  isLoading: boolean;
@@ -79,11 +93,15 @@ export function usePagedList<T>({
79
93
  pollIntervalMs,
80
94
  pollWhile,
81
95
  staleTime,
96
+ gcTime,
97
+ keepPreviousItems = false,
82
98
  }: UsePagedListOptions<T>): PagedList<T> {
83
99
  const query = useInfiniteQuery({
84
100
  queryKey,
85
101
  enabled,
86
102
  staleTime,
103
+ gcTime,
104
+ placeholderData: keepPreviousItems ? keepPreviousData : undefined,
87
105
  initialPageParam: undefined as string | undefined,
88
106
  queryFn: ({ pageParam }) => fetchPage(pageParam),
89
107
  getNextPageParam: (last: PagedPage<T>) => last.nextCursor,
@@ -99,8 +117,11 @@ export function usePagedList<T>({
99
117
  : false,
100
118
  });
101
119
 
102
- const pages = query.data?.pages ?? [];
103
- const items = pages.flatMap((p) => p.items);
120
+ const pages = query.data?.pages;
121
+ // Memoized on the query's data, which TanStack keeps referentially stable while nothing
122
+ // changed — flattening per render handed every consumer a new array and defeated its memos.
123
+ const items = useMemo(() => (pages ?? []).flatMap((p) => p.items), [pages]);
124
+ const total = useMemo(() => pages?.findLast((p) => p.total !== undefined)?.total, [pages]);
104
125
 
105
126
  return {
106
127
  items,
@@ -109,7 +130,7 @@ export function usePagedList<T>({
109
130
  isError: query.isError,
110
131
  error: query.error,
111
132
  hasMore: !!query.hasNextPage,
112
- total: pages.findLast((p) => p.total !== undefined)?.total,
133
+ total,
113
134
  loadMore: () => {
114
135
  if (query.hasNextPage && !query.isFetchingNextPage) void query.fetchNextPage();
115
136
  },