cursedbelt 5.21.0 → 5.22.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.
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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;
|
|
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.
|
|
3
|
+
"version": "5.22.0",
|
|
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.",
|
|
@@ -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
|
-
|
|
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
|
|
133
|
+
total,
|
|
113
134
|
loadMore: () => {
|
|
114
135
|
if (query.hasNextPage && !query.isFetchingNextPage) void query.fetchNextPage();
|
|
115
136
|
},
|