@umituz/react-native-design-system 2.6.110 → 2.6.111
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 +2 -2
- package/src/exports/infinite-scroll.ts +7 -0
- package/src/exports/uuid.ts +7 -0
- package/src/index.ts +10 -0
- package/src/infinite-scroll/domain/interfaces/infinite-scroll-list-props.ts +67 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-config.ts +108 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-return.ts +40 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-state.ts +58 -0
- package/src/infinite-scroll/domain/utils/pagination-utils.ts +63 -0
- package/src/infinite-scroll/domain/utils/type-guards.ts +53 -0
- package/src/infinite-scroll/index.ts +62 -0
- package/src/infinite-scroll/presentation/components/empty.tsx +44 -0
- package/src/infinite-scroll/presentation/components/error.tsx +66 -0
- package/src/infinite-scroll/presentation/components/infinite-scroll-list.tsx +120 -0
- package/src/infinite-scroll/presentation/components/loading-more.tsx +38 -0
- package/src/infinite-scroll/presentation/components/loading.tsx +40 -0
- package/src/infinite-scroll/presentation/components/types.ts +124 -0
- package/src/infinite-scroll/presentation/hooks/pagination.helper.ts +83 -0
- package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +327 -0
- package/src/uuid/index.ts +15 -0
- package/src/uuid/infrastructure/utils/UUIDUtils.ts +75 -0
- package/src/uuid/package-lock.json +14255 -0
- package/src/uuid/types/UUID.ts +36 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInfiniteScroll Hook
|
|
3
|
+
*
|
|
4
|
+
* Supports page-based and cursor-based pagination
|
|
5
|
+
* Features:
|
|
6
|
+
* - AbortController for request cancellation
|
|
7
|
+
* - Automatic retry with exponential backoff
|
|
8
|
+
* - Performance monitoring in __DEV__ mode
|
|
9
|
+
* - Memory leak prevention
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
13
|
+
import type { InfiniteScrollConfig } from "../../domain/types/infinite-scroll-config";
|
|
14
|
+
import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
|
|
15
|
+
import type { UseInfiniteScrollReturn } from "../../domain/types/infinite-scroll-return";
|
|
16
|
+
import { loadData, loadMoreData, isCursorMode } from "./pagination.helper";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CONFIG = {
|
|
19
|
+
pageSize: 20,
|
|
20
|
+
threshold: 5,
|
|
21
|
+
autoLoad: true,
|
|
22
|
+
initialPage: 0,
|
|
23
|
+
maxRetries: 3,
|
|
24
|
+
retryDelay: 1000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function createInitialState<T>(
|
|
28
|
+
initialPage: number,
|
|
29
|
+
totalItems?: number,
|
|
30
|
+
): InfiniteScrollState<T> {
|
|
31
|
+
return {
|
|
32
|
+
items: [],
|
|
33
|
+
pages: [],
|
|
34
|
+
currentPage: initialPage,
|
|
35
|
+
cursor: null,
|
|
36
|
+
hasMore: true,
|
|
37
|
+
isLoading: true,
|
|
38
|
+
isLoadingMore: false,
|
|
39
|
+
isRefreshing: false,
|
|
40
|
+
error: null,
|
|
41
|
+
totalItems,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sleep utility for retry delay
|
|
47
|
+
*/
|
|
48
|
+
function sleep(ms: number): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Retry logic with exponential backoff
|
|
54
|
+
*/
|
|
55
|
+
async function retryWithBackoff<T>(
|
|
56
|
+
fn: () => Promise<T>,
|
|
57
|
+
maxRetries: number,
|
|
58
|
+
baseDelay: number,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
let lastError: Error | undefined;
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
|
|
68
|
+
if (attempt < maxRetries) {
|
|
69
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.log(
|
|
72
|
+
`[useInfiniteScroll] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
await sleep(delay);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw lastError;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useInfiniteScroll<T>(
|
|
84
|
+
config: InfiniteScrollConfig<T>,
|
|
85
|
+
): UseInfiniteScrollReturn<T> {
|
|
86
|
+
const {
|
|
87
|
+
pageSize = DEFAULT_CONFIG.pageSize,
|
|
88
|
+
autoLoad = DEFAULT_CONFIG.autoLoad,
|
|
89
|
+
totalItems,
|
|
90
|
+
} = config;
|
|
91
|
+
|
|
92
|
+
const initialPage =
|
|
93
|
+
"initialPage" in config ? config.initialPage || 0 : DEFAULT_CONFIG.initialPage;
|
|
94
|
+
|
|
95
|
+
const maxRetries = DEFAULT_CONFIG.maxRetries;
|
|
96
|
+
const retryDelay = DEFAULT_CONFIG.retryDelay;
|
|
97
|
+
|
|
98
|
+
const [state, setState] = useState<InfiniteScrollState<T>>(() =>
|
|
99
|
+
createInitialState<T>(initialPage, totalItems),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const isLoadingRef = useRef(false);
|
|
103
|
+
const isMountedRef = useRef(true);
|
|
104
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
105
|
+
|
|
106
|
+
// Cleanup on unmount
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
isMountedRef.current = true;
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
isMountedRef.current = false;
|
|
112
|
+
abortControllerRef.current?.abort();
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
console.log("[useInfiniteScroll] Cleanup: component unmounted");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Cancel pending requests
|
|
120
|
+
const cancelPendingRequests = useCallback(() => {
|
|
121
|
+
abortControllerRef.current?.abort();
|
|
122
|
+
abortControllerRef.current = new AbortController();
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const loadInitial = useCallback(async () => {
|
|
126
|
+
if (isLoadingRef.current) return;
|
|
127
|
+
isLoadingRef.current = true;
|
|
128
|
+
|
|
129
|
+
cancelPendingRequests();
|
|
130
|
+
|
|
131
|
+
if (isMountedRef.current) {
|
|
132
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const newState = await retryWithBackoff(
|
|
139
|
+
async () => {
|
|
140
|
+
const result = await loadData(config, initialPage, pageSize, totalItems);
|
|
141
|
+
return result;
|
|
142
|
+
},
|
|
143
|
+
maxRetries,
|
|
144
|
+
retryDelay,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (isMountedRef.current) {
|
|
148
|
+
setState(newState);
|
|
149
|
+
|
|
150
|
+
if (__DEV__) {
|
|
151
|
+
const duration = performance.now() - startTime;
|
|
152
|
+
console.log(
|
|
153
|
+
`[useInfiniteScroll] Initial load completed in ${duration.toFixed(2)}ms`,
|
|
154
|
+
`Loaded ${newState.items.length} items`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isMountedRef.current) {
|
|
160
|
+
const errorMessage =
|
|
161
|
+
error instanceof Error ? error.message : "Failed to load data";
|
|
162
|
+
|
|
163
|
+
setState((prev) => ({
|
|
164
|
+
...prev,
|
|
165
|
+
isLoading: false,
|
|
166
|
+
error: errorMessage,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
if (__DEV__) {
|
|
170
|
+
console.error("[useInfiniteScroll] Load initial failed:", errorMessage);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
isLoadingRef.current = false;
|
|
175
|
+
}
|
|
176
|
+
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
177
|
+
|
|
178
|
+
const loadMore = useCallback(async () => {
|
|
179
|
+
if (
|
|
180
|
+
isLoadingRef.current ||
|
|
181
|
+
!state.hasMore ||
|
|
182
|
+
state.isLoadingMore ||
|
|
183
|
+
state.isLoading
|
|
184
|
+
) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isCursorMode(config) && !state.cursor) return;
|
|
189
|
+
|
|
190
|
+
isLoadingRef.current = true;
|
|
191
|
+
|
|
192
|
+
if (isMountedRef.current) {
|
|
193
|
+
setState((prev) => ({ ...prev, isLoadingMore: true, error: null }));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const updates = await retryWithBackoff(
|
|
200
|
+
async () => {
|
|
201
|
+
const result = await loadMoreData(config, state, pageSize);
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
maxRetries,
|
|
205
|
+
retryDelay,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (isMountedRef.current) {
|
|
209
|
+
setState((prev) => ({ ...prev, ...updates }));
|
|
210
|
+
|
|
211
|
+
if (__DEV__) {
|
|
212
|
+
const duration = performance.now() - startTime;
|
|
213
|
+
const newItemsCount = updates.items?.length || 0;
|
|
214
|
+
console.log(
|
|
215
|
+
`[useInfiniteScroll] Load more completed in ${duration.toFixed(2)}ms`,
|
|
216
|
+
`Loaded ${newItemsCount} items, total: ${updates.items?.length || 0}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (isMountedRef.current) {
|
|
222
|
+
const errorMessage =
|
|
223
|
+
error instanceof Error ? error.message : "Failed to load more items";
|
|
224
|
+
|
|
225
|
+
setState((prev) => ({
|
|
226
|
+
...prev,
|
|
227
|
+
isLoadingMore: false,
|
|
228
|
+
error: errorMessage,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
if (__DEV__) {
|
|
232
|
+
console.error("[useInfiniteScroll] Load more failed:", errorMessage);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
isLoadingRef.current = false;
|
|
237
|
+
}
|
|
238
|
+
}, [config, state, pageSize, maxRetries, retryDelay]);
|
|
239
|
+
|
|
240
|
+
const refresh = useCallback(async () => {
|
|
241
|
+
if (isLoadingRef.current) return;
|
|
242
|
+
isLoadingRef.current = true;
|
|
243
|
+
|
|
244
|
+
cancelPendingRequests();
|
|
245
|
+
|
|
246
|
+
if (isMountedRef.current) {
|
|
247
|
+
setState((prev) => ({ ...prev, isRefreshing: true, error: null }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const newState = await retryWithBackoff(
|
|
254
|
+
async () => {
|
|
255
|
+
const result = await loadData(config, initialPage, pageSize, totalItems);
|
|
256
|
+
return result;
|
|
257
|
+
},
|
|
258
|
+
maxRetries,
|
|
259
|
+
retryDelay,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (isMountedRef.current) {
|
|
263
|
+
setState(newState);
|
|
264
|
+
|
|
265
|
+
if (__DEV__) {
|
|
266
|
+
const duration = performance.now() - startTime;
|
|
267
|
+
console.log(
|
|
268
|
+
`[useInfiniteScroll] Refresh completed in ${duration.toFixed(2)}ms`,
|
|
269
|
+
`Loaded ${newState.items.length} items`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (isMountedRef.current) {
|
|
275
|
+
const errorMessage =
|
|
276
|
+
error instanceof Error ? error.message : "Failed to refresh data";
|
|
277
|
+
|
|
278
|
+
setState((prev) => ({
|
|
279
|
+
...prev,
|
|
280
|
+
isRefreshing: false,
|
|
281
|
+
error: errorMessage,
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
if (__DEV__) {
|
|
285
|
+
console.error("[useInfiniteScroll] Refresh failed:", errorMessage);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
isLoadingRef.current = false;
|
|
290
|
+
}
|
|
291
|
+
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
292
|
+
|
|
293
|
+
const reset = useCallback(() => {
|
|
294
|
+
isLoadingRef.current = false;
|
|
295
|
+
cancelPendingRequests();
|
|
296
|
+
setState(createInitialState<T>(initialPage, totalItems));
|
|
297
|
+
|
|
298
|
+
if (__DEV__) {
|
|
299
|
+
console.log("[useInfiniteScroll] State reset");
|
|
300
|
+
}
|
|
301
|
+
}, [initialPage, totalItems, cancelPendingRequests]);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (autoLoad) {
|
|
305
|
+
loadInitial();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return () => {
|
|
309
|
+
// Cleanup on config change
|
|
310
|
+
if (__DEV__) {
|
|
311
|
+
console.log("[useInfiniteScroll] Config changed, cleaning up");
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}, [autoLoad, loadInitial]);
|
|
315
|
+
|
|
316
|
+
const canLoadMore =
|
|
317
|
+
state.hasMore && !state.isLoadingMore && !state.isLoading;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
items: state.items,
|
|
321
|
+
state,
|
|
322
|
+
loadMore,
|
|
323
|
+
refresh,
|
|
324
|
+
reset,
|
|
325
|
+
canLoadMore,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native UUID
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform UUID generation for React Native apps
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
generateUUID,
|
|
9
|
+
generateUUID as uuidv4,
|
|
10
|
+
generateUUID as generateCreationId,
|
|
11
|
+
isValidUUID,
|
|
12
|
+
getUUIDVersion,
|
|
13
|
+
} from "./infrastructure/utils/UUIDUtils";
|
|
14
|
+
export type { UUID } from "./types/UUID";
|
|
15
|
+
export { UUIDVersion, UUID_CONSTANTS } from "./types/UUID";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID Generation Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-platform UUID generation using expo-crypto.
|
|
5
|
+
* Compatible with React Native (iOS, Android) and Web.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Crypto from 'expo-crypto';
|
|
9
|
+
import type { UUID } from '../../types/UUID';
|
|
10
|
+
import { UUID_CONSTANTS } from '../../types/UUID';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a v4 UUID
|
|
14
|
+
* Uses expo-crypto's randomUUID() for secure UUID generation
|
|
15
|
+
*
|
|
16
|
+
* @returns A v4 UUID string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { generateUUID } from '@umituz/react-native-uuid';
|
|
21
|
+
*
|
|
22
|
+
* const id = generateUUID();
|
|
23
|
+
* // Returns: "550e8400-e29b-41d4-a716-446655440000"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const generateUUID = (): UUID => {
|
|
27
|
+
return Crypto.randomUUID() as UUID;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate UUID format
|
|
32
|
+
* Checks if a string is a valid v4 UUID
|
|
33
|
+
*
|
|
34
|
+
* @param value - The value to validate
|
|
35
|
+
* @returns True if the value is a valid v4 UUID
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { isValidUUID } from '@umituz/react-native-uuid';
|
|
40
|
+
*
|
|
41
|
+
* isValidUUID('550e8400-e29b-41d4-a716-446655440000'); // true
|
|
42
|
+
* isValidUUID('invalid-uuid'); // false
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const isValidUUID = (value: string): value is UUID => {
|
|
46
|
+
return UUID_CONSTANTS.PATTERN.test(value);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get version from UUID string
|
|
51
|
+
* Returns the UUID version number (1-5) or null for NIL/invalid
|
|
52
|
+
*
|
|
53
|
+
* @param value - The UUID string
|
|
54
|
+
* @returns UUID version number or null
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { getUUIDVersion } from '@umituz/react-native-uuid';
|
|
59
|
+
*
|
|
60
|
+
* getUUIDVersion('550e8400-e29b-41d4-a716-446655440000'); // 4
|
|
61
|
+
* getUUIDVersion('00000000-0000-0000-0000-000000000000'); // 0 (NIL)
|
|
62
|
+
* getUUIDVersion('invalid'); // null
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const getUUIDVersion = (value: string): number | null => {
|
|
66
|
+
if (value === UUID_CONSTANTS.NIL) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionChar = value.charAt(14);
|
|
71
|
+
const version = parseInt(versionChar, 10);
|
|
72
|
+
|
|
73
|
+
return (version >= 1 && version <= 5) ? version : null;
|
|
74
|
+
};
|
|
75
|
+
|