Skip to content

Commit 992eda8

Browse files
committed
add support for infinite
1 parent 6da5615 commit 992eda8

12 files changed

+580
-298
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type {
2+
DataTag,
3+
DefaultError,
4+
InfiniteData,
5+
InitialDataFunction,
6+
NonUndefinedGuard,
7+
OmitKeyof,
8+
QueryKey,
9+
SkipToken,
10+
} from '@tanstack/query-core';
11+
12+
import type { UseInfiniteQueryOptions } from './types';
13+
14+
export type UndefinedInitialDataInfiniteOptions<
15+
TQueryFnData,
16+
TError = DefaultError,
17+
TData = InfiniteData<TQueryFnData>,
18+
TQueryKey extends QueryKey = QueryKey,
19+
TPageParam = unknown,
20+
> = UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
21+
initialData?:
22+
| undefined
23+
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
24+
| InitialDataFunction<NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>>;
25+
};
26+
27+
export type UnusedSkipTokenInfiniteOptions<
28+
TQueryFnData,
29+
TError = DefaultError,
30+
TData = InfiniteData<TQueryFnData>,
31+
TQueryKey extends QueryKey = QueryKey,
32+
TPageParam = unknown,
33+
> = OmitKeyof<UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, 'queryFn'> & {
34+
queryFn?: Exclude<
35+
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>['queryFn'],
36+
SkipToken | undefined
37+
>;
38+
};
39+
40+
export type DefinedInitialDataInfiniteOptions<
41+
TQueryFnData,
42+
TError = DefaultError,
43+
TData = InfiniteData<TQueryFnData>,
44+
TQueryKey extends QueryKey = QueryKey,
45+
TPageParam = unknown,
46+
> = UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
47+
initialData:
48+
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
49+
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
50+
| undefined;
51+
};
52+
53+
export function infiniteQueryOptions<
54+
TQueryFnData,
55+
TError = DefaultError,
56+
TData = InfiniteData<TQueryFnData>,
57+
TQueryKey extends QueryKey = QueryKey,
58+
TPageParam = unknown,
59+
>(
60+
options: DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
61+
): DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
62+
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
63+
};
64+
65+
export function infiniteQueryOptions<
66+
TQueryFnData,
67+
TError = DefaultError,
68+
TData = InfiniteData<TQueryFnData>,
69+
TQueryKey extends QueryKey = QueryKey,
70+
TPageParam = unknown,
71+
>(
72+
options: UnusedSkipTokenInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
73+
): UnusedSkipTokenInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
74+
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
75+
};
76+
77+
export function infiniteQueryOptions<
78+
TQueryFnData,
79+
TError = DefaultError,
80+
TData = InfiniteData<TQueryFnData>,
81+
TQueryKey extends QueryKey = QueryKey,
82+
TPageParam = unknown,
83+
>(
84+
options: UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
85+
): UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
86+
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
87+
};
88+
89+
/**
90+
*
91+
*/
92+
export function infiniteQueryOptions(options: unknown) {
93+
return options;
94+
}

packages/shared/src/react/clerk-rq/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type {
22
DefaultError,
3+
DefinedInfiniteQueryObserverResult,
34
DefinedQueryObserverResult,
45
InfiniteQueryObserverOptions,
6+
InfiniteQueryObserverResult,
57
OmitKeyof,
68
QueryKey,
79
QueryObserverOptions,
@@ -52,3 +54,10 @@ export type UseBaseQueryResult<TData = unknown, TError = DefaultError> = QueryOb
5254
export type UseQueryResult<TData = unknown, TError = DefaultError> = UseBaseQueryResult<TData, TError>;
5355

5456
export type DefinedUseQueryResult<TData = unknown, TError = DefaultError> = DefinedQueryObserverResult<TData, TError>;
57+
58+
export type UseInfiniteQueryResult<TData = unknown, TError = DefaultError> = InfiniteQueryObserverResult<TData, TError>;
59+
60+
export type DefinedUseInfiniteQueryResult<TData = unknown, TError = DefaultError> = DefinedInfiniteQueryObserverResult<
61+
TData,
62+
TError
63+
>;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client';
2+
3+
import type { DefaultError, InfiniteData, QueryKey, QueryObserver } from '@tanstack/query-core';
4+
import { InfiniteQueryObserver } from '@tanstack/query-core';
5+
6+
import type { DefinedInitialDataInfiniteOptions, UndefinedInitialDataInfiniteOptions } from './infiniteQueryOptions';
7+
import type { DefinedUseInfiniteQueryResult, UseInfiniteQueryOptions, UseInfiniteQueryResult } from './types';
8+
import { useBaseQuery } from './useBaseQuery';
9+
10+
export function useClerkInfiniteQuery<
11+
TQueryFnData,
12+
TError = DefaultError,
13+
TData = InfiniteData<TQueryFnData>,
14+
TQueryKey extends QueryKey = QueryKey,
15+
TPageParam = unknown,
16+
>(
17+
options: DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
18+
): DefinedUseInfiniteQueryResult<TData, TError>;
19+
20+
export function useClerkInfiniteQuery<
21+
TQueryFnData,
22+
TError = DefaultError,
23+
TData = InfiniteData<TQueryFnData>,
24+
TQueryKey extends QueryKey = QueryKey,
25+
TPageParam = unknown,
26+
>(
27+
options: UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
28+
): UseInfiniteQueryResult<TData, TError>;
29+
30+
export function useClerkInfiniteQuery<
31+
TQueryFnData,
32+
TError = DefaultError,
33+
TData = InfiniteData<TQueryFnData>,
34+
TQueryKey extends QueryKey = QueryKey,
35+
TPageParam = unknown,
36+
>(
37+
options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
38+
): UseInfiniteQueryResult<TData, TError>;
39+
/**
40+
*
41+
*/
42+
export function useClerkInfiniteQuery(options: UseInfiniteQueryOptions) {
43+
return useBaseQuery(options, InfiniteQueryObserver as unknown as typeof QueryObserver);
44+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { PagesOrInfiniteConfig, PagesOrInfiniteOptions, PaginatedResources } from '../types';
2+
3+
export type ArrayType<DataArray> = DataArray extends Array<infer ElementType> ? ElementType : never;
4+
5+
export type ExtractData<Type> = Type extends { data: infer Data } ? ArrayType<Data> : Type;
6+
7+
export type UsePagesOrInfiniteSignature = <
8+
Params extends PagesOrInfiniteOptions,
9+
FetcherReturnData extends Record<string, any>,
10+
CacheKeys extends Record<string, unknown> = Record<string, unknown>,
11+
TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig,
12+
>(
13+
params: Params,
14+
fetcher: ((p: Params) => FetcherReturnData | Promise<FetcherReturnData>) | undefined,
15+
config: TConfig,
16+
cacheKeys: CacheKeys,
17+
) => PaginatedResources<ExtractData<FetcherReturnData>, TConfig['infinite']>;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
'use client';
2+
3+
import type { ClerkPaginatedResponse } from '@clerk/types';
4+
import { useCallback, useMemo, useRef, useState } from 'react';
5+
6+
import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client';
7+
import { useClerkInfiniteQuery } from '../clerk-rq/useInfiniteQuery';
8+
import { useClerkQuery } from '../clerk-rq/useQuery';
9+
import type { CacheSetter, ValueOrSetter } from '../types';
10+
import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types';
11+
import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared';
12+
13+
export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => {
14+
const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1);
15+
16+
// Cache initialPage and initialPageSize until unmount
17+
const initialPageRef = useRef(params.initialPage ?? 1);
18+
const pageSizeRef = useRef(params.pageSize ?? 10);
19+
20+
const enabled = config.enabled ?? true;
21+
const triggerInfinite = config.infinite ?? false;
22+
// Support keepPreviousData
23+
const _keepPreviousData = config.keepPreviousData ?? false;
24+
25+
const [queryClient] = useClerkQueryClient();
26+
27+
// Non-infinite mode: single page query
28+
const pagesQueryKey = useMemo(() => {
29+
return [
30+
'clerk-pages',
31+
{
32+
...cacheKeys,
33+
...params,
34+
initialPage: paginatedPage,
35+
pageSize: pageSizeRef.current,
36+
},
37+
];
38+
}, [cacheKeys, params, paginatedPage]);
39+
40+
const singlePageQuery = useClerkQuery({
41+
queryKey: pagesQueryKey,
42+
queryFn: ({ queryKey }) => {
43+
const [, key] = queryKey as [string, Record<string, unknown>];
44+
45+
if (!fetcher) {
46+
return undefined as any;
47+
}
48+
49+
const requestParams = getDifferentKeys(key, cacheKeys);
50+
// console.log('-hehe', key, requestParams);
51+
52+
// @ts-ignore - params type differs slightly but is structurally compatible
53+
return fetcher(requestParams as Params);
54+
},
55+
staleTime: 60_000,
56+
enabled: enabled && !triggerInfinite && Boolean(fetcher),
57+
});
58+
59+
// Infinite mode: accumulate pages
60+
const infiniteQueryKey = useMemo(() => {
61+
return [
62+
'clerk-pages-infinite',
63+
{
64+
...cacheKeys,
65+
...params,
66+
},
67+
];
68+
}, [cacheKeys, params]);
69+
70+
const infiniteQuery = useClerkInfiniteQuery<ClerkPaginatedResponse<any>>({
71+
queryKey: infiniteQueryKey,
72+
initialPageParam: params.initialPage ?? 1,
73+
getNextPageParam: (lastPage, allPages, lastPageParam) => {
74+
const total = lastPage?.total_count ?? 0;
75+
const consumed = (allPages.length + (params.initialPage ? params.initialPage - 1 : 0)) * (params.pageSize ?? 10);
76+
return consumed < total ? (lastPageParam as number) + 1 : undefined;
77+
},
78+
queryFn: ({ pageParam }) => {
79+
if (!fetcher) {
80+
return undefined as any;
81+
}
82+
// @ts-ignore - merging page params for fetcher call
83+
return fetcher({ ...params, initialPage: pageParam, pageSize: pageSizeRef.current } as Params);
84+
},
85+
staleTime: 60_000,
86+
enabled: enabled && triggerInfinite && Boolean(fetcher),
87+
});
88+
89+
const page = useMemo(() => {
90+
if (triggerInfinite) {
91+
return (infiniteQuery.data?.pages?.length ?? 0) || 0;
92+
}
93+
return paginatedPage;
94+
}, [triggerInfinite, infiniteQuery.data?.pages?.length, paginatedPage]);
95+
96+
const fetchPage: ValueOrSetter<number> = useCallback(
97+
numberOrgFn => {
98+
if (triggerInfinite) {
99+
const next = typeof numberOrgFn === 'function' ? (numberOrgFn as (n: number) => number)(page) : numberOrgFn;
100+
const targetCount = Math.max(0, next);
101+
const currentCount = infiniteQuery.data?.pages?.length ?? 0;
102+
const toFetch = targetCount - currentCount;
103+
if (toFetch > 0) {
104+
void infiniteQuery.fetchNextPage({ cancelRefetch: false });
105+
}
106+
return;
107+
}
108+
return setPaginatedPage(numberOrgFn);
109+
},
110+
[infiniteQuery, page, triggerInfinite],
111+
);
112+
113+
const data = useMemo(() => {
114+
if (triggerInfinite) {
115+
return infiniteQuery.data?.pages?.map(a => a?.data).flat() ?? [];
116+
}
117+
return singlePageQuery.data?.data ?? [];
118+
}, [triggerInfinite, singlePageQuery.data, infiniteQuery.data]);
119+
120+
const count = useMemo(() => {
121+
if (triggerInfinite) {
122+
const pages = infiniteQuery.data?.pages ?? [];
123+
return pages[pages.length - 1]?.total_count || 0;
124+
}
125+
return singlePageQuery.data?.total_count ?? 0;
126+
}, [triggerInfinite, singlePageQuery.data, infiniteQuery.data]);
127+
128+
const isLoading = triggerInfinite ? infiniteQuery.isLoading : singlePageQuery.isLoading;
129+
const isFetching = triggerInfinite ? infiniteQuery.isFetching : singlePageQuery.isFetching;
130+
const error = (triggerInfinite ? (infiniteQuery.error as any) : (singlePageQuery.error as any)) ?? null;
131+
const isError = !!error;
132+
133+
const fetchNext = useCallback(() => {
134+
if (triggerInfinite) {
135+
void infiniteQuery.fetchNextPage({ cancelRefetch: false });
136+
return;
137+
}
138+
setPaginatedPage(n => Math.max(0, n + 1));
139+
}, [infiniteQuery, triggerInfinite]);
140+
141+
const fetchPrevious = useCallback(() => {
142+
if (triggerInfinite) {
143+
// not natively supported by forward-only pagination; noop
144+
return;
145+
}
146+
setPaginatedPage(n => Math.max(0, n - 1));
147+
}, [triggerInfinite]);
148+
149+
const offsetCount = (initialPageRef.current - 1) * pageSizeRef.current;
150+
const pageCount = Math.ceil((count - offsetCount) / pageSizeRef.current);
151+
const hasNextPage = triggerInfinite
152+
? Boolean(infiniteQuery.hasNextPage)
153+
: count - offsetCount * pageSizeRef.current > page * pageSizeRef.current;
154+
const hasPreviousPage = triggerInfinite
155+
? Boolean(infiniteQuery.hasPreviousPage)
156+
: (page - 1) * pageSizeRef.current > offsetCount * pageSizeRef.current;
157+
158+
const setData: CacheSetter = value => {
159+
if (triggerInfinite) {
160+
return queryClient.setQueryData(infiniteQueryKey, value) as any;
161+
}
162+
return queryClient.setQueryData(pagesQueryKey, value) as any;
163+
};
164+
165+
const revalidate = () => {
166+
if (triggerInfinite) {
167+
return queryClient.invalidateQueries({ queryKey: infiniteQueryKey });
168+
}
169+
return queryClient.invalidateQueries({ queryKey: pagesQueryKey });
170+
};
171+
172+
return {
173+
data,
174+
count,
175+
error,
176+
isLoading,
177+
isFetching,
178+
isError,
179+
page,
180+
pageCount,
181+
fetchPage,
182+
fetchNext,
183+
fetchPrevious,
184+
hasNextPage,
185+
hasPreviousPage,
186+
revalidate: revalidate as any,
187+
setData: setData as any,
188+
};
189+
};
190+
191+
export { useWithSafeValues };

0 commit comments

Comments
 (0)