@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.
@@ -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
+ }