@umituz/react-native-design-system 2.8.9 → 2.8.11
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/package.json +8 -1
- package/src/exports/tanstack.ts +1 -0
- package/src/index.ts +5 -0
- package/src/media/presentation/hooks/useCardMediaUpload.ts +1 -1
- package/src/media/presentation/hooks/useCardMultimediaFlashcard.ts +3 -3
- package/src/media/presentation/hooks/useMediaUpload.ts +1 -1
- package/src/media/presentation/hooks/useMultimediaFlashcard.ts +3 -3
- package/src/tanstack/domain/constants/CacheDefaults.ts +63 -0
- package/src/tanstack/domain/repositories/BaseRepository.ts +280 -0
- package/src/tanstack/domain/repositories/RepositoryFactory.ts +135 -0
- package/src/tanstack/domain/types/CacheStrategy.ts +115 -0
- package/src/tanstack/domain/utils/ErrorHelpers.ts +154 -0
- package/src/tanstack/domain/utils/QueryKeyFactory.ts +134 -0
- package/src/tanstack/domain/utils/TypeUtilities.ts +153 -0
- package/src/tanstack/index.ts +161 -0
- package/src/tanstack/infrastructure/config/PersisterConfig.ts +162 -0
- package/src/tanstack/infrastructure/config/QueryClientConfig.ts +154 -0
- package/src/tanstack/infrastructure/config/QueryClientSingleton.ts +69 -0
- package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +274 -0
- package/src/tanstack/infrastructure/providers/TanstackProvider.tsx +105 -0
- package/src/tanstack/presentation/hooks/useInvalidateQueries.ts +128 -0
- package/src/tanstack/presentation/hooks/useOptimisticUpdate.ts +88 -0
- package/src/tanstack/presentation/hooks/usePaginatedQuery.ts +129 -0
- package/src/tanstack/presentation/hooks/usePrefetch.ts +237 -0
- package/src/tanstack/presentation/utils/RetryHelpers.ts +67 -0
- package/src/tanstack/types/global.d.ts +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
4
|
+
import type { Persister } from '@tanstack/react-query-persist-client';
|
|
5
|
+
import { createQueryClient, type QueryClientFactoryOptions } from '../config/QueryClientConfig';
|
|
6
|
+
import { createPersister, type PersisterFactoryOptions } from '../config/PersisterConfig';
|
|
7
|
+
import { setGlobalQueryClient } from '../config/QueryClientSingleton';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TanStack provider props
|
|
11
|
+
*/
|
|
12
|
+
export interface TanstackProviderProps {
|
|
13
|
+
/**
|
|
14
|
+
* Child components
|
|
15
|
+
*/
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Custom QueryClient instance
|
|
20
|
+
* If not provided, a default one will be created
|
|
21
|
+
*/
|
|
22
|
+
queryClient?: QueryClient;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* QueryClient configuration options
|
|
26
|
+
* Only used if queryClient is not provided
|
|
27
|
+
*/
|
|
28
|
+
queryClientOptions?: QueryClientFactoryOptions;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enable AsyncStorage persistence
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
enablePersistence?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom persister instance
|
|
38
|
+
* Only used if enablePersistence is true
|
|
39
|
+
*/
|
|
40
|
+
persister?: Persister;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Persister configuration options
|
|
44
|
+
* Only used if enablePersistence is true and persister is not provided
|
|
45
|
+
*/
|
|
46
|
+
persisterOptions?: PersisterFactoryOptions;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Callback when persistence is successfully restored
|
|
50
|
+
*/
|
|
51
|
+
onPersistSuccess?: () => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callback when persistence restoration fails
|
|
55
|
+
*/
|
|
56
|
+
onPersistError?: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* TanStack Query provider with optional AsyncStorage persistence
|
|
61
|
+
*/
|
|
62
|
+
export function TanstackProvider({
|
|
63
|
+
children,
|
|
64
|
+
queryClient: providedQueryClient,
|
|
65
|
+
queryClientOptions,
|
|
66
|
+
enablePersistence = true,
|
|
67
|
+
persister: providedPersister,
|
|
68
|
+
persisterOptions,
|
|
69
|
+
onPersistSuccess,
|
|
70
|
+
onPersistError,
|
|
71
|
+
}: TanstackProviderProps): React.ReactElement {
|
|
72
|
+
// Create QueryClient if not provided and set as global singleton
|
|
73
|
+
const [queryClient] = React.useState(() => {
|
|
74
|
+
const client = providedQueryClient ?? createQueryClient(queryClientOptions);
|
|
75
|
+
setGlobalQueryClient(client);
|
|
76
|
+
return client;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Create persister if persistence is enabled
|
|
80
|
+
const [persister] = React.useState(() => {
|
|
81
|
+
if (!enablePersistence) return undefined;
|
|
82
|
+
return providedPersister ?? createPersister(persisterOptions);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Without persistence
|
|
86
|
+
if (!enablePersistence || !persister) {
|
|
87
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// With persistence
|
|
91
|
+
return (
|
|
92
|
+
<PersistQueryClientProvider
|
|
93
|
+
client={queryClient}
|
|
94
|
+
persistOptions={{
|
|
95
|
+
persister,
|
|
96
|
+
maxAge: persisterOptions?.maxAge,
|
|
97
|
+
buster: persisterOptions?.busterVersion,
|
|
98
|
+
}}
|
|
99
|
+
onSuccess={onPersistSuccess}
|
|
100
|
+
onError={onPersistError}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</PersistQueryClientProvider>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInvalidateQueries Hook
|
|
3
|
+
* Presentation layer - Cache invalidation helper
|
|
4
|
+
*
|
|
5
|
+
* General-purpose cache invalidation for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useCallback } from 'react';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook for easy cache invalidation
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const invalidate = useInvalidateQueries();
|
|
17
|
+
*
|
|
18
|
+
* // Invalidate all posts queries
|
|
19
|
+
* await invalidate(['posts']);
|
|
20
|
+
*
|
|
21
|
+
* // Invalidate specific post
|
|
22
|
+
* await invalidate(['posts', 'detail', 123]);
|
|
23
|
+
*
|
|
24
|
+
* // Invalidate with predicate
|
|
25
|
+
* await invalidate({
|
|
26
|
+
* predicate: (query) => query.queryKey[0] === 'posts'
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function useInvalidateQueries() {
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
return useCallback(
|
|
34
|
+
async (queryKey: readonly unknown[]) => {
|
|
35
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
36
|
+
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
|
|
39
|
+
console.log('[TanStack Query] Invalidated queries:', queryKey);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[queryClient],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hook for invalidating multiple query patterns at once
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const invalidateMultiple = useInvalidateMultipleQueries();
|
|
52
|
+
*
|
|
53
|
+
* // Invalidate posts and comments
|
|
54
|
+
* await invalidateMultiple([['posts'], ['comments']]);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useInvalidateMultipleQueries() {
|
|
58
|
+
const queryClient = useQueryClient();
|
|
59
|
+
|
|
60
|
+
return useCallback(
|
|
61
|
+
async (queryKeys: Array<readonly unknown[]>) => {
|
|
62
|
+
await Promise.all(
|
|
63
|
+
queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key })),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
|
|
68
|
+
console.log('[TanStack Query] Invalidated multiple queries:', queryKeys);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[queryClient],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hook for removing queries from cache
|
|
77
|
+
* More aggressive than invalidation - completely removes the data
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const removeQueries = useRemoveQueries();
|
|
82
|
+
*
|
|
83
|
+
* // Remove all posts queries
|
|
84
|
+
* await removeQueries(['posts']);
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function useRemoveQueries() {
|
|
88
|
+
const queryClient = useQueryClient();
|
|
89
|
+
|
|
90
|
+
return useCallback(
|
|
91
|
+
async (queryKey: readonly unknown[]) => {
|
|
92
|
+
queryClient.removeQueries({ queryKey });
|
|
93
|
+
|
|
94
|
+
if (__DEV__) {
|
|
95
|
+
|
|
96
|
+
console.log('[TanStack Query] Removed queries:', queryKey);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[queryClient],
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Hook for resetting queries to their initial state
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const resetQueries = useResetQueries();
|
|
109
|
+
*
|
|
110
|
+
* // Reset all posts queries
|
|
111
|
+
* await resetQueries(['posts']);
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function useResetQueries() {
|
|
115
|
+
const queryClient = useQueryClient();
|
|
116
|
+
|
|
117
|
+
return useCallback(
|
|
118
|
+
async (queryKey: readonly unknown[]) => {
|
|
119
|
+
await queryClient.resetQueries({ queryKey });
|
|
120
|
+
|
|
121
|
+
if (__DEV__) {
|
|
122
|
+
|
|
123
|
+
console.log('[TanStack Query] Reset queries:', queryKey);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[queryClient],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOptimisticUpdate Hook
|
|
3
|
+
* Presentation layer - Optimistic update helper
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Optimistic update configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface OptimisticUpdateConfig<TData, TVariables, TError = Error> {
|
|
12
|
+
queryKey: readonly unknown[];
|
|
13
|
+
updater: (oldData: TData | undefined, variables: TVariables) => TData;
|
|
14
|
+
invalidateOnSuccess?: boolean;
|
|
15
|
+
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
16
|
+
onError?: (error: TError, variables: TVariables, context: unknown) => void;
|
|
17
|
+
onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: unknown) => void;
|
|
18
|
+
onSuccess?: (data: TData, variables: TVariables, context: unknown) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for mutations with optimistic updates and automatic rollback
|
|
23
|
+
*/
|
|
24
|
+
export function useOptimisticUpdate<TData = unknown, TVariables = unknown, TError = Error>(
|
|
25
|
+
config: OptimisticUpdateConfig<TData, TVariables, TError>,
|
|
26
|
+
) {
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
const { queryKey, updater, invalidateOnSuccess = true, onError, onSettled, onSuccess, mutationFn } = config;
|
|
29
|
+
|
|
30
|
+
return useMutation({
|
|
31
|
+
mutationFn,
|
|
32
|
+
onMutate: async (variables: TVariables) => {
|
|
33
|
+
await queryClient.cancelQueries({ queryKey });
|
|
34
|
+
const previousData = queryClient.getQueryData<TData>(queryKey);
|
|
35
|
+
|
|
36
|
+
if (previousData !== undefined) {
|
|
37
|
+
const optimisticData = updater(previousData, variables);
|
|
38
|
+
queryClient.setQueryData(queryKey, optimisticData);
|
|
39
|
+
|
|
40
|
+
if (__DEV__) {
|
|
41
|
+
|
|
42
|
+
console.log('[TanStack Query] Optimistic update applied:', queryKey);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { previousData };
|
|
47
|
+
},
|
|
48
|
+
onError: (error: TError, variables: TVariables, context: unknown) => {
|
|
49
|
+
|
|
50
|
+
const ctx = context as any;
|
|
51
|
+
if (ctx?.previousData !== undefined) {
|
|
52
|
+
queryClient.setQueryData(queryKey, ctx.previousData);
|
|
53
|
+
|
|
54
|
+
if (__DEV__) {
|
|
55
|
+
|
|
56
|
+
console.error('[TanStack Query] Optimistic update rolled back:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (onError) {
|
|
61
|
+
onError(error, variables, context);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
onSuccess: (data: TData, variables: TVariables, context: unknown) => {
|
|
65
|
+
if (onSuccess) {
|
|
66
|
+
onSuccess(data, variables, context);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
onSettled: (data: TData | undefined, error: TError | null, variables: TVariables, context: unknown) => {
|
|
70
|
+
if (invalidateOnSuccess && !error) {
|
|
71
|
+
queryClient.invalidateQueries({ queryKey });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (onSettled) {
|
|
75
|
+
onSettled(data, error, variables, context);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Hook for list mutations with optimistic updates
|
|
83
|
+
*/
|
|
84
|
+
export function useOptimisticListUpdate<TData extends unknown[], TVariables = unknown>(
|
|
85
|
+
config: OptimisticUpdateConfig<TData, TVariables, Error>,
|
|
86
|
+
) {
|
|
87
|
+
return useOptimisticUpdate<TData, TVariables, Error>(config);
|
|
88
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePaginatedQuery Hook
|
|
3
|
+
* Presentation layer - Pagination helper
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Page parameter for cursor-based pagination
|
|
11
|
+
*/
|
|
12
|
+
export interface CursorPageParam {
|
|
13
|
+
cursor?: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Page parameter for offset-based pagination
|
|
19
|
+
*/
|
|
20
|
+
export interface OffsetPageParam {
|
|
21
|
+
offset: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Paginated response with cursor
|
|
27
|
+
*/
|
|
28
|
+
export interface CursorPaginatedResponse<TData> {
|
|
29
|
+
items: TData[];
|
|
30
|
+
nextCursor?: string;
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Paginated response with offset
|
|
36
|
+
*/
|
|
37
|
+
export interface OffsetPaginatedResponse<TData> {
|
|
38
|
+
items: TData[];
|
|
39
|
+
total: number;
|
|
40
|
+
offset: number;
|
|
41
|
+
limit: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cursor pagination options
|
|
46
|
+
*/
|
|
47
|
+
export interface CursorPaginationOptions<TData> {
|
|
48
|
+
queryKey: readonly unknown[];
|
|
49
|
+
queryFn: (context: { pageParam?: string }) => Promise<CursorPaginatedResponse<TData>>;
|
|
50
|
+
limit?: number;
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
staleTime?: number;
|
|
53
|
+
gcTime?: number;
|
|
54
|
+
refetchOnMount?: boolean | 'always';
|
|
55
|
+
refetchOnWindowFocus?: boolean | 'always';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hook for cursor-based infinite scroll
|
|
60
|
+
*/
|
|
61
|
+
export function useCursorPagination<TData>(options: CursorPaginationOptions<TData>) {
|
|
62
|
+
const { queryKey, queryFn, ...restOptions } = options;
|
|
63
|
+
|
|
64
|
+
const result = useInfiniteQuery({
|
|
65
|
+
queryKey,
|
|
66
|
+
queryFn: ({ pageParam }) => queryFn({ pageParam }),
|
|
67
|
+
initialPageParam: undefined as string | undefined,
|
|
68
|
+
getNextPageParam: (lastPage: CursorPaginatedResponse<TData>) =>
|
|
69
|
+
lastPage.hasMore ? lastPage.nextCursor : undefined,
|
|
70
|
+
...restOptions,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const flatData = useMemo(() => {
|
|
74
|
+
if (!result.data?.pages) return [];
|
|
75
|
+
return result.data.pages.flatMap((page) => page.items);
|
|
76
|
+
}, [result.data]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...result,
|
|
80
|
+
flatData,
|
|
81
|
+
totalItems: flatData.length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Offset pagination options
|
|
87
|
+
*/
|
|
88
|
+
export interface OffsetPaginationOptions<TData> {
|
|
89
|
+
queryKey: readonly unknown[];
|
|
90
|
+
queryFn: (context: { pageParam: OffsetPageParam }) => Promise<OffsetPaginatedResponse<TData>>;
|
|
91
|
+
limit?: number;
|
|
92
|
+
enabled?: boolean;
|
|
93
|
+
staleTime?: number;
|
|
94
|
+
gcTime?: number;
|
|
95
|
+
refetchOnMount?: boolean | 'always';
|
|
96
|
+
refetchOnWindowFocus?: boolean | 'always';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Hook for offset-based pagination
|
|
101
|
+
*/
|
|
102
|
+
export function useOffsetPagination<TData>(options: OffsetPaginationOptions<TData>) {
|
|
103
|
+
const { queryKey, queryFn, limit = 20, ...restOptions } = options;
|
|
104
|
+
|
|
105
|
+
const result = useInfiniteQuery({
|
|
106
|
+
queryKey,
|
|
107
|
+
queryFn: ({ pageParam }) => queryFn({ pageParam }),
|
|
108
|
+
initialPageParam: { offset: 0, limit },
|
|
109
|
+
getNextPageParam: (lastPage: OffsetPaginatedResponse<TData>) => {
|
|
110
|
+
const nextOffset = lastPage.offset + lastPage.limit;
|
|
111
|
+
return nextOffset < lastPage.total ? { offset: nextOffset, limit } : undefined;
|
|
112
|
+
},
|
|
113
|
+
...restOptions,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const flatData = useMemo(() => {
|
|
117
|
+
if (!result.data?.pages) return [];
|
|
118
|
+
return result.data.pages.flatMap((page) => page.items);
|
|
119
|
+
}, [result.data]);
|
|
120
|
+
|
|
121
|
+
const total = result.data?.pages?.[result.data.pages.length - 1]?.total ?? 0;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...result,
|
|
125
|
+
flatData,
|
|
126
|
+
totalItems: flatData.length,
|
|
127
|
+
total,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePrefetch Hook
|
|
3
|
+
* Presentation layer - Query prefetching utilities
|
|
4
|
+
*
|
|
5
|
+
* General-purpose prefetching for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
10
|
+
import type { QueryKey, QueryFunction } from '@tanstack/react-query';
|
|
11
|
+
|
|
12
|
+
export interface PrefetchOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Time in ms that the prefetched data should stay fresh
|
|
15
|
+
*/
|
|
16
|
+
staleTime?: number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Time in ms that unused data stays in cache
|
|
20
|
+
*/
|
|
21
|
+
gcTime?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook for prefetching query data
|
|
26
|
+
*
|
|
27
|
+
* Useful for:
|
|
28
|
+
* - Preloading data before navigation
|
|
29
|
+
* - Warming up cache on mount
|
|
30
|
+
* - Background data refresh
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* function UserProfileList({ userIds }: { userIds: string[] }) {
|
|
35
|
+
* const prefetchUser = usePrefetchQuery(['user'], async (id) => fetchUser(id));
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <FlatList
|
|
39
|
+
* data={userIds}
|
|
40
|
+
* onViewableItemsChanged={({ viewableItems }) => {
|
|
41
|
+
* viewableItems.forEach((item) => {
|
|
42
|
+
* prefetchUser(item.key);
|
|
43
|
+
* });
|
|
44
|
+
* }}
|
|
45
|
+
* />
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function usePrefetchQuery<
|
|
51
|
+
TQueryFnData = unknown,
|
|
52
|
+
TVariables = string | number,
|
|
53
|
+
>(
|
|
54
|
+
queryKey: QueryKey,
|
|
55
|
+
queryFn: (variables: TVariables) => Promise<TQueryFnData>,
|
|
56
|
+
options: PrefetchOptions = {},
|
|
57
|
+
) {
|
|
58
|
+
const queryClient = useQueryClient();
|
|
59
|
+
const prefetchingRef = new Set<TVariables>();
|
|
60
|
+
|
|
61
|
+
const prefetch = useCallback(
|
|
62
|
+
async (variables: TVariables) => {
|
|
63
|
+
if (prefetchingRef.has(variables)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
prefetchingRef.add(variables);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await queryClient.prefetchQuery({
|
|
71
|
+
queryKey: [...queryKey, variables],
|
|
72
|
+
queryFn: () => queryFn(variables),
|
|
73
|
+
staleTime: options.staleTime,
|
|
74
|
+
gcTime: options.gcTime,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (__DEV__) {
|
|
78
|
+
|
|
79
|
+
console.log('[TanStack Query] Prefetched:', [...queryKey, variables]);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
prefetchingRef.delete(variables);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[queryClient, queryKey, queryFn, options.staleTime, options.gcTime],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return prefetch;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Hook for prefetching infinite query data
|
|
93
|
+
*
|
|
94
|
+
* Useful for:
|
|
95
|
+
* - Preloading infinite scroll content
|
|
96
|
+
* - Warming up paginated feeds
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* function FeedScreen() {
|
|
101
|
+
* const prefetchFeed = usePrefetchInfiniteQuery(
|
|
102
|
+
* ['feed'],
|
|
103
|
+
* ({ pageParam }) => fetchFeed({ cursor: pageParam })
|
|
104
|
+
* );
|
|
105
|
+
*
|
|
106
|
+
* useEffect(() => {
|
|
107
|
+
* prefetchFeed();
|
|
108
|
+
* }, []);
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function usePrefetchInfiniteQuery<
|
|
113
|
+
TQueryFnData = unknown,
|
|
114
|
+
TPageParam = unknown,
|
|
115
|
+
>(
|
|
116
|
+
queryKey: QueryKey,
|
|
117
|
+
queryFn: QueryFunction<TQueryFnData, QueryKey, TPageParam>,
|
|
118
|
+
options: PrefetchOptions = {},
|
|
119
|
+
) {
|
|
120
|
+
const queryClient = useQueryClient();
|
|
121
|
+
const hasPrefetchedRef = useRef(false);
|
|
122
|
+
|
|
123
|
+
const prefetch = useCallback(async () => {
|
|
124
|
+
if (hasPrefetchedRef.current) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasPrefetchedRef.current = true;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await queryClient.prefetchInfiniteQuery({
|
|
132
|
+
queryKey,
|
|
133
|
+
queryFn,
|
|
134
|
+
staleTime: options.staleTime,
|
|
135
|
+
gcTime: options.gcTime,
|
|
136
|
+
initialPageParam: undefined as unknown as TPageParam,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (__DEV__) {
|
|
140
|
+
|
|
141
|
+
console.log('[TanStack Query] Prefetched infinite:', queryKey);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
hasPrefetchedRef.current = false;
|
|
145
|
+
}
|
|
146
|
+
}, [queryClient, queryKey, queryFn, options.staleTime, options.gcTime]);
|
|
147
|
+
|
|
148
|
+
return prefetch;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hook for prefetching on mount
|
|
153
|
+
*
|
|
154
|
+
* Convenience hook that prefetches data when component mounts
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* function UserProfile({ userId }: { userId: string }) {
|
|
159
|
+
* usePrefetchOnMount(
|
|
160
|
+
* ['user', userId],
|
|
161
|
+
* () => fetchUser(userId),
|
|
162
|
+
* { staleTime: TIME_MS.MINUTE }
|
|
163
|
+
* );
|
|
164
|
+
*
|
|
165
|
+
* // Component will prefetch user data on mount
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function usePrefetchOnMount<TData = unknown>(
|
|
170
|
+
queryKey: QueryKey,
|
|
171
|
+
queryFn: () => Promise<TData>,
|
|
172
|
+
options: PrefetchOptions = {},
|
|
173
|
+
) {
|
|
174
|
+
const queryClient = useQueryClient();
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
queryClient.prefetchQuery({
|
|
178
|
+
queryKey,
|
|
179
|
+
queryFn,
|
|
180
|
+
staleTime: options.staleTime,
|
|
181
|
+
gcTime: options.gcTime,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (__DEV__) {
|
|
185
|
+
|
|
186
|
+
console.log('[TanStack Query] Prefetched on mount:', queryKey);
|
|
187
|
+
}
|
|
188
|
+
}, [queryClient, queryKey, queryFn, options.staleTime, options.gcTime]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Hook for prefetching multiple queries
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* function Dashboard() {
|
|
197
|
+
* const prefetchMultiple = usePrefetchMultiple();
|
|
198
|
+
*
|
|
199
|
+
* useEffect(() => {
|
|
200
|
+
* prefetchMultiple([
|
|
201
|
+
* { queryKey: ['user'], queryFn: fetchUser },
|
|
202
|
+
* { queryKey: ['posts'], queryFn: fetchPosts },
|
|
203
|
+
* { queryKey: ['notifications'], queryFn: fetchNotifications },
|
|
204
|
+
* ]);
|
|
205
|
+
* }, []);
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function usePrefetchMultiple<TData = unknown>() {
|
|
210
|
+
const queryClient = useQueryClient();
|
|
211
|
+
|
|
212
|
+
return useCallback(
|
|
213
|
+
async (queries: Array<{
|
|
214
|
+
queryKey: QueryKey;
|
|
215
|
+
queryFn: () => Promise<TData>;
|
|
216
|
+
staleTime?: number;
|
|
217
|
+
gcTime?: number;
|
|
218
|
+
}>) => {
|
|
219
|
+
await Promise.all(
|
|
220
|
+
queries.map(({ queryKey, queryFn, staleTime, gcTime }) =>
|
|
221
|
+
queryClient.prefetchQuery({
|
|
222
|
+
queryKey,
|
|
223
|
+
queryFn,
|
|
224
|
+
staleTime,
|
|
225
|
+
gcTime,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (__DEV__) {
|
|
231
|
+
|
|
232
|
+
console.log('[TanStack Query] Prefetched multiple:', queries.length);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
[queryClient],
|
|
236
|
+
);
|
|
237
|
+
}
|