@umituz/react-native-tanstack 1.0.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.
- package/README.md +206 -0
- package/package.json +58 -0
- package/src/domain/constants/CacheDefaults.ts +63 -0
- package/src/domain/types/CacheStrategy.ts +115 -0
- package/src/domain/utils/QueryKeyFactory.ts +134 -0
- package/src/index.ts +88 -0
- package/src/infrastructure/config/PersisterConfig.ts +162 -0
- package/src/infrastructure/config/QueryClientConfig.ts +148 -0
- package/src/infrastructure/providers/TanstackProvider.tsx +142 -0
- package/src/presentation/hooks/useInvalidateQueries.ts +128 -0
- package/src/presentation/hooks/useOptimisticUpdate.ts +126 -0
- package/src/presentation/hooks/usePaginatedQuery.ts +156 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persister Configuration
|
|
3
|
+
* Infrastructure layer - AsyncStorage persistence setup
|
|
4
|
+
*
|
|
5
|
+
* General-purpose persistence configuration for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
9
|
+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
|
10
|
+
import { DEFAULT_GC_TIME } from '../../domain/constants/CacheDefaults';
|
|
11
|
+
import type { Persister } from '@tanstack/react-query-persist-client';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Persister factory options
|
|
15
|
+
*/
|
|
16
|
+
export interface PersisterFactoryOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Storage key prefix
|
|
19
|
+
* @default 'tanstack-query'
|
|
20
|
+
*/
|
|
21
|
+
keyPrefix?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum age of cached data in milliseconds
|
|
25
|
+
* Data older than this will be discarded on restore
|
|
26
|
+
* @default 24 hours
|
|
27
|
+
*/
|
|
28
|
+
maxAge?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cache version for invalidation
|
|
32
|
+
* Increment this to invalidate all existing caches
|
|
33
|
+
* @default 1
|
|
34
|
+
*/
|
|
35
|
+
busterVersion?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Throttle time for persistence writes (in ms)
|
|
39
|
+
* Prevents excessive writes to AsyncStorage
|
|
40
|
+
* @default 1000
|
|
41
|
+
*/
|
|
42
|
+
throttleTime?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create an AsyncStorage persister for TanStack Query
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const persister = createPersister({
|
|
51
|
+
* keyPrefix: 'myapp',
|
|
52
|
+
* maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
53
|
+
* busterVersion: '1',
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function createPersister(options: PersisterFactoryOptions = {}): Persister {
|
|
58
|
+
const {
|
|
59
|
+
keyPrefix = 'tanstack-query',
|
|
60
|
+
maxAge = DEFAULT_GC_TIME.LONG,
|
|
61
|
+
busterVersion = '1',
|
|
62
|
+
throttleTime = 1000,
|
|
63
|
+
} = options;
|
|
64
|
+
|
|
65
|
+
return createAsyncStoragePersister({
|
|
66
|
+
storage: AsyncStorage,
|
|
67
|
+
key: `${keyPrefix}-cache`,
|
|
68
|
+
throttleTime,
|
|
69
|
+
serialize: (data) => {
|
|
70
|
+
// Add metadata for cache validation
|
|
71
|
+
const persistData = {
|
|
72
|
+
version: busterVersion,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
data,
|
|
75
|
+
};
|
|
76
|
+
return JSON.stringify(persistData);
|
|
77
|
+
},
|
|
78
|
+
deserialize: (cachedString) => {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(cachedString);
|
|
81
|
+
|
|
82
|
+
// Validate cache version
|
|
83
|
+
if (parsed.version !== busterVersion) {
|
|
84
|
+
if (__DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(
|
|
87
|
+
`[TanStack Query] Cache version mismatch. Expected: ${busterVersion}, Got: ${parsed.version}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validate cache age
|
|
94
|
+
const age = Date.now() - parsed.timestamp;
|
|
95
|
+
if (age > maxAge) {
|
|
96
|
+
if (__DEV__) {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.log(`[TanStack Query] Cache expired. Age: ${age}ms, Max: ${maxAge}ms`);
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parsed.data;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (__DEV__) {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.error('[TanStack Query] Failed to deserialize cache:', error);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear all persisted cache data
|
|
117
|
+
* Useful for logout or cache reset scenarios
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* await clearPersistedCache('myapp');
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export async function clearPersistedCache(keyPrefix: string = 'tanstack-query'): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
await AsyncStorage.removeItem(`${keyPrefix}-cache`);
|
|
127
|
+
if (__DEV__) {
|
|
128
|
+
// eslint-disable-next-line no-console
|
|
129
|
+
console.log(`[TanStack Query] Cleared persisted cache: ${keyPrefix}`);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (__DEV__) {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.error('[TanStack Query] Failed to clear persisted cache:', error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get persisted cache size (in bytes)
|
|
141
|
+
* Useful for monitoring storage usage
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* const size = await getPersistedCacheSize('myapp');
|
|
146
|
+
* console.log(`Cache size: ${size} bytes`);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export async function getPersistedCacheSize(
|
|
150
|
+
keyPrefix: string = 'tanstack-query',
|
|
151
|
+
): Promise<number> {
|
|
152
|
+
try {
|
|
153
|
+
const data = await AsyncStorage.getItem(`${keyPrefix}-cache`);
|
|
154
|
+
return data ? new Blob([data]).size : 0;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (__DEV__) {
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.error('[TanStack Query] Failed to get cache size:', error);
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryClient Configuration
|
|
3
|
+
* Infrastructure layer - TanStack Query client setup
|
|
4
|
+
*
|
|
5
|
+
* General-purpose QueryClient configuration for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_STALE_TIME,
|
|
11
|
+
DEFAULT_GC_TIME,
|
|
12
|
+
DEFAULT_RETRY,
|
|
13
|
+
} from '../../domain/constants/CacheDefaults';
|
|
14
|
+
import type { CacheConfig, CacheStrategyType } from '../../domain/types/CacheStrategy';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Cache strategy configurations
|
|
18
|
+
*/
|
|
19
|
+
export const CacheStrategies: Record<CacheStrategyType, CacheConfig> = {
|
|
20
|
+
REALTIME: {
|
|
21
|
+
staleTime: DEFAULT_STALE_TIME.REALTIME,
|
|
22
|
+
gcTime: DEFAULT_GC_TIME.VERY_SHORT,
|
|
23
|
+
refetchOnMount: 'always',
|
|
24
|
+
refetchOnWindowFocus: true,
|
|
25
|
+
refetchOnReconnect: true,
|
|
26
|
+
retry: DEFAULT_RETRY.MINIMAL,
|
|
27
|
+
},
|
|
28
|
+
USER_DATA: {
|
|
29
|
+
staleTime: DEFAULT_STALE_TIME.MEDIUM,
|
|
30
|
+
gcTime: DEFAULT_GC_TIME.LONG,
|
|
31
|
+
refetchOnMount: false,
|
|
32
|
+
refetchOnWindowFocus: false,
|
|
33
|
+
refetchOnReconnect: true,
|
|
34
|
+
retry: DEFAULT_RETRY.STANDARD,
|
|
35
|
+
},
|
|
36
|
+
MASTER_DATA: {
|
|
37
|
+
staleTime: DEFAULT_STALE_TIME.VERY_LONG,
|
|
38
|
+
gcTime: DEFAULT_GC_TIME.VERY_LONG,
|
|
39
|
+
refetchOnMount: false,
|
|
40
|
+
refetchOnWindowFocus: false,
|
|
41
|
+
refetchOnReconnect: false,
|
|
42
|
+
retry: DEFAULT_RETRY.STANDARD,
|
|
43
|
+
},
|
|
44
|
+
PUBLIC_DATA: {
|
|
45
|
+
staleTime: DEFAULT_STALE_TIME.MEDIUM,
|
|
46
|
+
gcTime: DEFAULT_GC_TIME.LONG,
|
|
47
|
+
refetchOnMount: false,
|
|
48
|
+
refetchOnWindowFocus: false,
|
|
49
|
+
refetchOnReconnect: true,
|
|
50
|
+
retry: DEFAULT_RETRY.STANDARD,
|
|
51
|
+
},
|
|
52
|
+
CUSTOM: {
|
|
53
|
+
staleTime: DEFAULT_STALE_TIME.SHORT,
|
|
54
|
+
gcTime: DEFAULT_GC_TIME.MEDIUM,
|
|
55
|
+
refetchOnMount: false,
|
|
56
|
+
refetchOnWindowFocus: false,
|
|
57
|
+
refetchOnReconnect: true,
|
|
58
|
+
retry: DEFAULT_RETRY.STANDARD,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* QueryClient factory options
|
|
64
|
+
*/
|
|
65
|
+
export interface QueryClientFactoryOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Default staleTime in milliseconds
|
|
68
|
+
* @default 5 minutes
|
|
69
|
+
*/
|
|
70
|
+
defaultStaleTime?: number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default gcTime in milliseconds
|
|
74
|
+
* @default 24 hours
|
|
75
|
+
*/
|
|
76
|
+
defaultGcTime?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default retry configuration
|
|
80
|
+
* @default 3
|
|
81
|
+
*/
|
|
82
|
+
defaultRetry?: boolean | number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Enable development mode logging
|
|
86
|
+
* @default __DEV__
|
|
87
|
+
*/
|
|
88
|
+
enableDevLogging?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a configured QueryClient instance
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const queryClient = createQueryClient({
|
|
97
|
+
* defaultStaleTime: 5 * 60 * 1000, // 5 minutes
|
|
98
|
+
* enableDevLogging: true,
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function createQueryClient(options: QueryClientFactoryOptions = {}): QueryClient {
|
|
103
|
+
const {
|
|
104
|
+
defaultStaleTime = DEFAULT_STALE_TIME.SHORT,
|
|
105
|
+
defaultGcTime = DEFAULT_GC_TIME.LONG,
|
|
106
|
+
defaultRetry = DEFAULT_RETRY.STANDARD,
|
|
107
|
+
enableDevLogging = __DEV__,
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
return new QueryClient({
|
|
111
|
+
defaultOptions: {
|
|
112
|
+
queries: {
|
|
113
|
+
staleTime: defaultStaleTime,
|
|
114
|
+
gcTime: defaultGcTime,
|
|
115
|
+
retry: defaultRetry,
|
|
116
|
+
refetchOnWindowFocus: false,
|
|
117
|
+
refetchOnMount: false,
|
|
118
|
+
refetchOnReconnect: true,
|
|
119
|
+
},
|
|
120
|
+
mutations: {
|
|
121
|
+
retry: DEFAULT_RETRY.MINIMAL,
|
|
122
|
+
onError: (error) => {
|
|
123
|
+
if (enableDevLogging) {
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
console.error('[TanStack Query] Mutation error:', error);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get cache configuration for a specific strategy
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const config = getCacheStrategy('PUBLIC_DATA');
|
|
139
|
+
* const { data } = useQuery({
|
|
140
|
+
* queryKey: ['posts'],
|
|
141
|
+
* queryFn: fetchPosts,
|
|
142
|
+
* ...config,
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function getCacheStrategy(strategy: CacheStrategyType): CacheConfig {
|
|
147
|
+
return CacheStrategies[strategy];
|
|
148
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Provider
|
|
3
|
+
* Infrastructure layer - Root provider component
|
|
4
|
+
*
|
|
5
|
+
* General-purpose provider for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
10
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
11
|
+
import type { Persister } from '@tanstack/react-query-persist-client';
|
|
12
|
+
import { createQueryClient, type QueryClientFactoryOptions } from '../config/QueryClientConfig';
|
|
13
|
+
import { createPersister, type PersisterFactoryOptions } from '../config/PersisterConfig';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TanStack provider props
|
|
17
|
+
*/
|
|
18
|
+
export interface TanstackProviderProps {
|
|
19
|
+
/**
|
|
20
|
+
* Child components
|
|
21
|
+
*/
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Custom QueryClient instance
|
|
26
|
+
* If not provided, a default one will be created
|
|
27
|
+
*/
|
|
28
|
+
queryClient?: QueryClient;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* QueryClient configuration options
|
|
32
|
+
* Only used if queryClient is not provided
|
|
33
|
+
*/
|
|
34
|
+
queryClientOptions?: QueryClientFactoryOptions;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enable AsyncStorage persistence
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
enablePersistence?: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom persister instance
|
|
44
|
+
* Only used if enablePersistence is true
|
|
45
|
+
*/
|
|
46
|
+
persister?: Persister;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Persister configuration options
|
|
50
|
+
* Only used if enablePersistence is true and persister is not provided
|
|
51
|
+
*/
|
|
52
|
+
persisterOptions?: PersisterFactoryOptions;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Callback when persistence is successfully restored
|
|
56
|
+
*/
|
|
57
|
+
onPersistSuccess?: () => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Callback when persistence restoration fails
|
|
61
|
+
*/
|
|
62
|
+
onPersistError?: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* TanStack Query provider with optional AsyncStorage persistence
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Basic usage (with persistence)
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <TanstackProvider>
|
|
72
|
+
* <App />
|
|
73
|
+
* </TanstackProvider>
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Custom configuration
|
|
78
|
+
* ```tsx
|
|
79
|
+
* <TanstackProvider
|
|
80
|
+
* queryClientOptions={{
|
|
81
|
+
* defaultStaleTime: 10 * 60 * 1000,
|
|
82
|
+
* enableDevLogging: true,
|
|
83
|
+
* }}
|
|
84
|
+
* persisterOptions={{
|
|
85
|
+
* keyPrefix: 'myapp',
|
|
86
|
+
* maxAge: 24 * 60 * 60 * 1000,
|
|
87
|
+
* busterVersion: '2',
|
|
88
|
+
* }}
|
|
89
|
+
* onPersistSuccess={() => console.log('Cache restored')}
|
|
90
|
+
* >
|
|
91
|
+
* <App />
|
|
92
|
+
* </TanstackProvider>
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Without persistence
|
|
97
|
+
* ```tsx
|
|
98
|
+
* <TanstackProvider enablePersistence={false}>
|
|
99
|
+
* <App />
|
|
100
|
+
* </TanstackProvider>
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function TanstackProvider({
|
|
104
|
+
children,
|
|
105
|
+
queryClient: providedQueryClient,
|
|
106
|
+
queryClientOptions,
|
|
107
|
+
enablePersistence = true,
|
|
108
|
+
persister: providedPersister,
|
|
109
|
+
persisterOptions,
|
|
110
|
+
onPersistSuccess,
|
|
111
|
+
onPersistError,
|
|
112
|
+
}: TanstackProviderProps): JSX.Element {
|
|
113
|
+
// Create QueryClient if not provided
|
|
114
|
+
const [queryClient] = React.useState(() => providedQueryClient ?? createQueryClient(queryClientOptions));
|
|
115
|
+
|
|
116
|
+
// Create persister if persistence is enabled
|
|
117
|
+
const [persister] = React.useState(() => {
|
|
118
|
+
if (!enablePersistence) return undefined;
|
|
119
|
+
return providedPersister ?? createPersister(persisterOptions);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Without persistence
|
|
123
|
+
if (!enablePersistence || !persister) {
|
|
124
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// With persistence
|
|
128
|
+
return (
|
|
129
|
+
<PersistQueryClientProvider
|
|
130
|
+
client={queryClient}
|
|
131
|
+
persistOptions={{
|
|
132
|
+
persister,
|
|
133
|
+
maxAge: persisterOptions?.maxAge,
|
|
134
|
+
buster: persisterOptions?.busterVersion,
|
|
135
|
+
}}
|
|
136
|
+
onSuccess={onPersistSuccess}
|
|
137
|
+
onError={onPersistError}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</PersistQueryClientProvider>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -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
|
+
// eslint-disable-next-line no-console
|
|
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
|
+
// eslint-disable-next-line no-console
|
|
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
|
+
// eslint-disable-next-line no-console
|
|
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
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.log('[TanStack Query] Reset queries:', queryKey);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[queryClient],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOptimisticUpdate Hook
|
|
3
|
+
* Presentation layer - Optimistic update helper
|
|
4
|
+
*
|
|
5
|
+
* General-purpose optimistic updates for any React Native app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMutation, useQueryClient, type UseMutationOptions } from '@tanstack/react-query';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Optimistic update configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface OptimisticUpdateConfig<TData, TVariables> {
|
|
14
|
+
/**
|
|
15
|
+
* Query key to update optimistically
|
|
16
|
+
*/
|
|
17
|
+
queryKey: readonly unknown[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Function to update the cached data optimistically
|
|
21
|
+
*/
|
|
22
|
+
updater: (oldData: TData | undefined, variables: TVariables) => TData;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether to invalidate the query after successful mutation
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
invalidateOnSuccess?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hook for mutations with optimistic updates and automatic rollback
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const updatePost = useOptimisticUpdate<Post, UpdatePostVariables>({
|
|
37
|
+
* mutationFn: (variables) => api.updatePost(variables.id, variables.data),
|
|
38
|
+
* queryKey: ['posts', postId],
|
|
39
|
+
* updater: (oldPost, variables) => ({
|
|
40
|
+
* ...oldPost,
|
|
41
|
+
* ...variables.data,
|
|
42
|
+
* }),
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Usage
|
|
46
|
+
* updatePost.mutate({ id: 123, data: { title: 'New Title' } });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function useOptimisticUpdate<TData = unknown, TVariables = unknown, TError = Error>(
|
|
50
|
+
config: OptimisticUpdateConfig<TData, TVariables> &
|
|
51
|
+
UseMutationOptions<TData, TError, TVariables>,
|
|
52
|
+
) {
|
|
53
|
+
const queryClient = useQueryClient();
|
|
54
|
+
const { queryKey, updater, invalidateOnSuccess = true, onError, onSettled, ...mutationOptions } = config;
|
|
55
|
+
|
|
56
|
+
return useMutation({
|
|
57
|
+
...mutationOptions,
|
|
58
|
+
onMutate: async (variables) => {
|
|
59
|
+
// Cancel outgoing refetches to avoid overwriting optimistic update
|
|
60
|
+
await queryClient.cancelQueries({ queryKey });
|
|
61
|
+
|
|
62
|
+
// Snapshot the previous value
|
|
63
|
+
const previousData = queryClient.getQueryData<TData>(queryKey);
|
|
64
|
+
|
|
65
|
+
// Optimistically update to the new value
|
|
66
|
+
if (previousData !== undefined) {
|
|
67
|
+
const optimisticData = updater(previousData, variables);
|
|
68
|
+
queryClient.setQueryData(queryKey, optimisticData);
|
|
69
|
+
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.log('[TanStack Query] Optimistic update applied:', queryKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Return context with previous data for rollback
|
|
77
|
+
return { previousData };
|
|
78
|
+
},
|
|
79
|
+
onError: (error, variables, context, ...rest) => {
|
|
80
|
+
// Rollback to previous data on error
|
|
81
|
+
if (context?.previousData !== undefined) {
|
|
82
|
+
queryClient.setQueryData(queryKey, context.previousData);
|
|
83
|
+
|
|
84
|
+
if (__DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.error('[TanStack Query] Optimistic update rolled back:', error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Call user-provided onError
|
|
91
|
+
if (onError) {
|
|
92
|
+
onError(error, variables, context, ...rest);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onSettled: (data, error, variables, context, ...rest) => {
|
|
96
|
+
// Invalidate query to refetch with real data
|
|
97
|
+
if (invalidateOnSuccess && !error) {
|
|
98
|
+
queryClient.invalidateQueries({ queryKey });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Call user-provided onSettled
|
|
102
|
+
if (onSettled) {
|
|
103
|
+
onSettled(data, error, variables, context, ...rest);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Hook for list mutations with optimistic updates (add/remove/update items)
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const addPost = useOptimisticListUpdate<Post[], { title: string }>({
|
|
115
|
+
* mutationFn: (variables) => api.createPost(variables),
|
|
116
|
+
* queryKey: ['posts'],
|
|
117
|
+
* updater: (oldPosts, newPost) => [...(oldPosts ?? []), newPost],
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function useOptimisticListUpdate<TData extends unknown[], TVariables = unknown>(
|
|
122
|
+
config: OptimisticUpdateConfig<TData, TVariables> &
|
|
123
|
+
UseMutationOptions<TData, Error, TVariables>,
|
|
124
|
+
) {
|
|
125
|
+
return useOptimisticUpdate<TData, TVariables>(config);
|
|
126
|
+
}
|