@umituz/react-native-tanstack 1.2.17 → 1.2.18

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 CHANGED
@@ -5,12 +5,16 @@ TanStack Query configuration and utilities for React Native apps with AsyncStora
5
5
  ## Features
6
6
 
7
7
  - ✅ **Pre-configured QueryClient** - Sensible defaults out of the box
8
- - ✅ **AsyncStorage Persistence** - Automatic cache restoration on app restart
8
+ - ✅ **AsyncStorage Persistence** - Automatic cache restoration on app restart via @umituz/react-native-storage
9
9
  - ✅ **Cache Strategies** - Pre-defined strategies for different data types
10
10
  - ✅ **Query Key Factories** - Type-safe key generation patterns
11
11
  - ✅ **Pagination Helpers** - Cursor and offset-based pagination
12
12
  - ✅ **Optimistic Updates** - Easy optimistic UI with automatic rollback
13
- - ✅ **Dev Tools** - Built-in logging for development
13
+ - ✅ **Prefetch Hooks** - Data preloading for better UX
14
+ - ✅ **Repository Pattern** - Base repository for CRUD operations
15
+ - ✅ **Error Helpers** - User-friendly error messages and error parsing
16
+ - ✅ **Type Utilities** - Type extractors for better type safety
17
+ - ✅ **Dev Monitor** - Query performance tracking (DEV only)
14
18
  - ✅ **General Purpose** - Works with Firebase, REST, GraphQL, any async data source
15
19
 
16
20
  ## Installation
@@ -201,6 +205,143 @@ function LikeButton({ postId }) {
201
205
 
202
206
  See [TypeScript definitions](./src/index.ts) for complete API documentation.
203
207
 
208
+ ---
209
+
210
+ ## Repository Pattern
211
+
212
+ Create type-safe repositories for your data:
213
+
214
+ ```typescript
215
+ import { BaseRepository, RepositoryFactory } from '@umituz/react-native-tanstack';
216
+
217
+ interface User {
218
+ id: string;
219
+ name: string;
220
+ email: string;
221
+ }
222
+
223
+ class UserRepository extends BaseRepository<User, CreateUserVars, UpdateUserVars> {
224
+ constructor() {
225
+ super('users', {
226
+ cacheStrategy: CacheStrategies.USER_DATA,
227
+ });
228
+ }
229
+
230
+ async fetchAll(params?: ListParams): Promise<User[]> {
231
+ const res = await fetch('/api/users');
232
+ return res.json();
233
+ }
234
+
235
+ async fetchById(id: string): Promise<User> {
236
+ const res = await fetch(`/api/users/${id}`);
237
+ return res.json();
238
+ }
239
+
240
+ async create({ variables }: CreateParams<CreateUserVars>): Promise<User> {
241
+ const res = await fetch('/api/users', {
242
+ method: 'POST',
243
+ body: JSON.stringify(variables),
244
+ });
245
+ return res.json();
246
+ }
247
+
248
+ async update({ id, variables }: UpdateParams<UpdateUserVars>): Promise<User> {
249
+ const res = await fetch(`/api/users/${id}`, {
250
+ method: 'PUT',
251
+ body: JSON.stringify(variables),
252
+ });
253
+ return res.json();
254
+ }
255
+
256
+ async remove(id: string): Promise<void> {
257
+ await fetch(`/api/users/${id}`, { method: 'DELETE' });
258
+ }
259
+ }
260
+
261
+ // Register repository
262
+ const userRepo = new UserRepository();
263
+ RepositoryFactory.register('users', userRepo);
264
+
265
+ // Use repository
266
+ const users = await userRepo.queryAll();
267
+ await userRepo.invalidateAll();
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Prefetching
273
+
274
+ Preload data before navigation:
275
+
276
+ ```typescript
277
+ import { usePrefetchQuery } from '@umituz/react-native-tanstack';
278
+
279
+ function UserList({ userIds }: { userIds: string[] }) {
280
+ const prefetchUser = usePrefetchQuery(['user'], async (id: string) => {
281
+ const res = await fetch(`/api/users/${id}`);
282
+ return res.json();
283
+ });
284
+
285
+ const handlePress = (userId: string) => {
286
+ // Prefetch before navigation
287
+ prefetchUser(userId);
288
+ navigation.navigate('UserDetail', { userId });
289
+ };
290
+
291
+ return <FlatList data={userIds} renderItem={({ item }) => (
292
+ <TouchableOpacity onPress={() => handlePress(item)}>
293
+ <Text>User {item}</Text>
294
+ </TouchableOpacity>
295
+ )} />;
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Error Handling
302
+
303
+ Get user-friendly error messages:
304
+
305
+ ```typescript
306
+ import { useMutation, getUserFriendlyMessage } from '@umituz/react-native-tanstack';
307
+
308
+ function CreateUserForm() {
309
+ const mutation = useMutation({
310
+ mutationFn: (data) => api.post('/users', data),
311
+ onError: (error) => {
312
+ const message = getUserFriendlyMessage(error);
313
+ showToast(message);
314
+ },
315
+ });
316
+
317
+ return <Button onPress={() => mutation.mutate(formData)}>Create</Button>;
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Dev Monitor
324
+
325
+ Track query performance in development:
326
+
327
+ ```typescript
328
+ import { DevMonitor } from '@umituz/react-native-tanstack';
329
+
330
+ // In your app setup
331
+ if (__DEV__) {
332
+ DevMonitor.attach(queryClient);
333
+
334
+ // Log performance report
335
+ DevMonitor.logReport();
336
+
337
+ // Get slow queries
338
+ const slowQueries = DevMonitor.getSlowQueries();
339
+
340
+ // Get cache stats
341
+ const stats = DevMonitor.getCacheStats();
342
+ }
343
+ ```
344
+
204
345
  ## License
205
346
 
206
347
  MIT © Ümit UZ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-tanstack",
3
- "version": "1.2.17",
3
+ "version": "1.2.18",
4
4
  "description": "TanStack Query configuration and utilities for React Native apps - Pre-configured QueryClient with AsyncStorage persistence",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Base Repository
3
+ * Domain layer - Abstract repository for data operations
4
+ *
5
+ * Provides generic CRUD operations with TanStack Query integration.
6
+ * Subclass this for specific entities to get type-safe data operations.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * class UserRepository extends BaseRepository<User, CreateUserVars, UpdateUserVars> {
11
+ * constructor() {
12
+ * super('users');
13
+ * }
14
+ *
15
+ * async fetchAll(): Promise<User[]> {
16
+ * return api.get('/users');
17
+ * }
18
+ *
19
+ * async fetchById(id: string): Promise<User> {
20
+ * return api.get(`/users/${id}`);
21
+ * }
22
+ *
23
+ * async create(data: CreateUserVars): Promise<User> {
24
+ * return api.post('/users', data);
25
+ * }
26
+ *
27
+ * async update(id: string, data: UpdateUserVars): Promise<User> {
28
+ * return api.put(`/users/${id}`, data);
29
+ * }
30
+ *
31
+ * async remove(id: string): Promise<void> {
32
+ * return api.delete(`/users/${id}`);
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+
38
+ import type { QueryClient } from '@tanstack/react-query';
39
+ import { getGlobalQueryClient } from '../../infrastructure/config/QueryClientSingleton';
40
+ import type { CacheConfig } from '../types/CacheStrategy';
41
+ import { CacheStrategies } from '../../infrastructure/config/QueryClientConfig';
42
+
43
+ export interface CreateParams<TVariables> {
44
+ variables: TVariables;
45
+ }
46
+
47
+ export interface UpdateParams<TVariables> {
48
+ id: string | number;
49
+ variables: TVariables;
50
+ }
51
+
52
+ export interface ListParams {
53
+ page?: number;
54
+ limit?: number;
55
+ filter?: Record<string, unknown>;
56
+ }
57
+
58
+ export interface RepositoryOptions {
59
+ /**
60
+ * Cache strategy for queries
61
+ * @default CacheStrategies.PUBLIC_DATA
62
+ */
63
+ cacheStrategy?: CacheConfig;
64
+
65
+ /**
66
+ * Stale time override
67
+ */
68
+ staleTime?: number;
69
+
70
+ /**
71
+ * GC time override
72
+ */
73
+ gcTime?: number;
74
+ }
75
+
76
+ /**
77
+ * Base repository for CRUD operations
78
+ *
79
+ * @template TData - Entity type
80
+ * @template TCreateVariables - Variables for create mutation
81
+ * @template TUpdateVariables - Variables for update mutation
82
+ */
83
+ export abstract class BaseRepository<
84
+ TData,
85
+ TCreateVariables = unknown,
86
+ TUpdateVariables = Partial<TData>,
87
+ > {
88
+ protected readonly resource: string;
89
+ protected readonly options: RepositoryOptions;
90
+
91
+ /**
92
+ * Query key factory for this repository
93
+ */
94
+ public readonly keys: {
95
+ all: () => readonly [string];
96
+ lists: () => readonly [string, 'list'];
97
+ list: (params: ListParams) => readonly [string, 'list', ListParams];
98
+ details: () => readonly [string, 'detail'];
99
+ detail: (id: string | number) => readonly [string, 'detail', string | number];
100
+ };
101
+
102
+ constructor(resource: string, options: RepositoryOptions = {}) {
103
+ this.resource = resource;
104
+ this.options = {
105
+ cacheStrategy: options.cacheStrategy ?? CacheStrategies.PUBLIC_DATA,
106
+ ...options,
107
+ };
108
+
109
+ this.keys = {
110
+ all: () => [this.resource] as const,
111
+ lists: () => [this.resource, 'list'] as const,
112
+ list: (params: ListParams) => [this.resource, 'list', params] as const,
113
+ details: () => [this.resource, 'detail'] as const,
114
+ detail: (id: string | number) => [this.resource, 'detail', id] as const,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Get query client instance
120
+ */
121
+ protected getClient(): QueryClient {
122
+ return getGlobalQueryClient();
123
+ }
124
+
125
+ /**
126
+ * Get cache options for queries
127
+ */
128
+ protected getCacheOptions(): { staleTime: number; gcTime: number } {
129
+ return {
130
+ staleTime: this.options.staleTime ?? (this.options.cacheStrategy?.staleTime ?? CacheStrategies.PUBLIC_DATA.staleTime),
131
+ gcTime: this.options.gcTime ?? (this.options.cacheStrategy?.gcTime ?? CacheStrategies.PUBLIC_DATA.gcTime),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Fetch all items - to be implemented by subclass
137
+ */
138
+ abstract fetchAll(params?: ListParams): Promise<TData[]>;
139
+
140
+ /**
141
+ * Fetch item by ID - to be implemented by subclass
142
+ */
143
+ abstract fetchById(id: string | number): Promise<TData>;
144
+
145
+ /**
146
+ * Create item - to be implemented by subclass
147
+ */
148
+ abstract create(params: CreateParams<TCreateVariables>): Promise<TData>;
149
+
150
+ /**
151
+ * Update item - to be implemented by subclass
152
+ */
153
+ abstract update(params: UpdateParams<TUpdateVariables>): Promise<TData>;
154
+
155
+ /**
156
+ * Delete item - to be implemented by subclass
157
+ */
158
+ abstract remove(id: string | number): Promise<void>;
159
+
160
+ /**
161
+ * Query all items with caching
162
+ */
163
+ async queryAll(params?: ListParams): Promise<TData[]> {
164
+ const client = this.getClient();
165
+ const queryKey = params ? this.keys.list(params) : this.keys.lists();
166
+ const cacheOptions = this.getCacheOptions();
167
+
168
+ return client.fetchQuery({
169
+ queryKey,
170
+ queryFn: () => this.fetchAll(params),
171
+ ...cacheOptions,
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Query item by ID with caching
177
+ */
178
+ async queryById(id: string | number): Promise<TData | undefined> {
179
+ const client = this.getClient();
180
+ const queryKey = this.keys.detail(id);
181
+ const cacheOptions = this.getCacheOptions();
182
+
183
+ try {
184
+ return client.fetchQuery({
185
+ queryKey,
186
+ queryFn: () => this.fetchById(id),
187
+ ...cacheOptions,
188
+ });
189
+ } catch {
190
+ return undefined;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Prefetch all items
196
+ */
197
+ async prefetchAll(params?: ListParams): Promise<void> {
198
+ const client = this.getClient();
199
+ const queryKey = params ? this.keys.list(params) : this.keys.lists();
200
+ const cacheOptions = this.getCacheOptions();
201
+
202
+ await client.prefetchQuery({
203
+ queryKey,
204
+ queryFn: () => this.fetchAll(params),
205
+ ...cacheOptions,
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Prefetch item by ID
211
+ */
212
+ async prefetchById(id: string | number): Promise<void> {
213
+ const client = this.getClient();
214
+ const queryKey = this.keys.detail(id);
215
+ const cacheOptions = this.getCacheOptions();
216
+
217
+ await client.prefetchQuery({
218
+ queryKey,
219
+ queryFn: () => this.fetchById(id),
220
+ ...cacheOptions,
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Invalidate all queries for this resource
226
+ */
227
+ invalidateAll(): Promise<void> {
228
+ const client = this.getClient();
229
+ return client.invalidateQueries({
230
+ predicate: (query) => {
231
+ const key = query.queryKey[0] as string;
232
+ return key === this.resource;
233
+ },
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Invalidate list queries
239
+ */
240
+ invalidateLists(): Promise<void> {
241
+ const client = this.getClient();
242
+ return client.invalidateQueries({
243
+ queryKey: this.keys.lists(),
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Invalidate detail query
249
+ */
250
+ invalidateDetail(id: string | number): Promise<void> {
251
+ const client = this.getClient();
252
+ return client.invalidateQueries({
253
+ queryKey: this.keys.detail(id),
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Set query data (optimistic update)
259
+ */
260
+ setData(id: string | number, data: TData): void {
261
+ const client = this.getClient();
262
+ client.setQueryData(this.keys.detail(id), data);
263
+ }
264
+
265
+ /**
266
+ * Get query data from cache
267
+ */
268
+ getData(id: string | number): TData | undefined {
269
+ const client = this.getClient();
270
+ return client.getQueryData<TData>(this.keys.detail(id));
271
+ }
272
+
273
+ /**
274
+ * Remove query data from cache
275
+ */
276
+ clearData(id: string | number): void {
277
+ const client = this.getClient();
278
+ client.setQueryData(this.keys.detail(id), undefined);
279
+ }
280
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Repository Factory
3
+ * Domain layer - Factory for creating repository instances
4
+ *
5
+ * Provides singleton instances of repositories to avoid multiple instances.
6
+ * Repositories are registered once and reused throughout the app.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // Register repositories
11
+ * RepositoryFactory.register('users', new UserRepository());
12
+ * RepositoryFactory.register('posts', new PostRepository());
13
+ *
14
+ * // Get repository
15
+ * const userRepo = RepositoryFactory.get<UserRepository>('users');
16
+ * const users = await userRepo.queryAll();
17
+ *
18
+ * // Get all registered keys
19
+ * const keys = RepositoryFactory.keys(); // ['users', 'posts']
20
+ *
21
+ * // Invalidate all repositories
22
+ * RepositoryFactory.invalidateAll();
23
+ * ```
24
+ */
25
+
26
+ import type { BaseRepository } from './BaseRepository';
27
+ import { getGlobalQueryClient } from '../../infrastructure/config/QueryClientSingleton';
28
+
29
+ type RepositoryMap = Map<string, BaseRepository<unknown, unknown, unknown>>;
30
+
31
+ class RepositoryFactoryClass {
32
+ private repositories: RepositoryMap = new Map();
33
+
34
+ /**
35
+ * Register a repository instance
36
+ *
37
+ * @param key - Unique identifier for the repository
38
+ * @param repository - Repository instance
39
+ */
40
+ register<TRepository extends BaseRepository<unknown, unknown, unknown>>(
41
+ key: string,
42
+ repository: TRepository,
43
+ ): void {
44
+ if (this.repositories.has(key)) {
45
+ if (__DEV__) {
46
+ // eslint-disable-next-line no-console
47
+ console.warn(
48
+ `[RepositoryFactory] Repository "${key}" is already registered. Overwriting.`,
49
+ );
50
+ }
51
+ }
52
+ this.repositories.set(key, repository);
53
+ }
54
+
55
+ /**
56
+ * Get a registered repository instance
57
+ *
58
+ * @param key - Repository identifier
59
+ * @throws Error if repository not found
60
+ */
61
+ get<TRepository extends BaseRepository<unknown, unknown, unknown>>(
62
+ key: string,
63
+ ): TRepository {
64
+ const repository = this.repositories.get(key);
65
+ if (!repository) {
66
+ throw new Error(
67
+ `[RepositoryFactory] Repository "${key}" not found. Make sure to register it first.`,
68
+ );
69
+ }
70
+ return repository as TRepository;
71
+ }
72
+
73
+ /**
74
+ * Check if repository is registered
75
+ */
76
+ has(key: string): boolean {
77
+ return this.repositories.has(key);
78
+ }
79
+
80
+ /**
81
+ * Unregister a repository
82
+ */
83
+ unregister(key: string): boolean {
84
+ if (__DEV__ && !this.repositories.has(key)) {
85
+ // eslint-disable-next-line no-console
86
+ console.warn(`[RepositoryFactory] Repository "${key}" is not registered.`);
87
+ }
88
+ return this.repositories.delete(key);
89
+ }
90
+
91
+ /**
92
+ * Get all registered repository keys
93
+ */
94
+ keys(): string[] {
95
+ return Array.from(this.repositories.keys());
96
+ }
97
+
98
+ /**
99
+ * Clear all registered repositories
100
+ * Useful for testing or cleanup
101
+ */
102
+ clear(): void {
103
+ this.repositories.clear();
104
+ }
105
+
106
+ /**
107
+ * Invalidate all queries from all registered repositories
108
+ */
109
+ async invalidateAll(): Promise<void> {
110
+ const client = getGlobalQueryClient();
111
+ await client.invalidateQueries();
112
+ }
113
+
114
+ /**
115
+ * Prefetch all data from all registered repositories
116
+ * Useful for app initialization or online event
117
+ */
118
+ async prefetchAll(): Promise<void> {
119
+ const promises = Array.from(this.repositories.values()).map((repo) => {
120
+ try {
121
+ return repo.prefetchAll();
122
+ } catch {
123
+ // Ignore prefetch errors
124
+ return Promise.resolve();
125
+ }
126
+ });
127
+
128
+ await Promise.all(promises);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Global repository factory instance
134
+ */
135
+ export const RepositoryFactory = new RepositoryFactoryClass();