@umituz/react-native-design-system 4.23.113 → 4.23.115

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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/AtomicTouchable.tsx +22 -0
  3. package/src/atoms/badge/AtomicBadge.tsx +26 -28
  4. package/src/atoms/chip/AtomicChip.tsx +5 -5
  5. package/src/atoms/datepicker/components/DatePickerModal.tsx +4 -3
  6. package/src/atoms/input/hooks/useInputState.ts +1 -1
  7. package/src/atoms/picker/components/PickerModal.tsx +1 -1
  8. package/src/atoms/picker/hooks/usePickerState.ts +28 -15
  9. package/src/atoms/skeleton/AtomicSkeleton.tsx +5 -5
  10. package/src/device/infrastructure/services/DeviceCapabilityService.ts +1 -12
  11. package/src/filesystem/infrastructure/services/directory.service.ts +37 -9
  12. package/src/filesystem/infrastructure/services/download.service.ts +62 -11
  13. package/src/filesystem/infrastructure/services/file-manager.service.ts +42 -11
  14. package/src/filesystem/infrastructure/services/file-writer.service.ts +8 -3
  15. package/src/media/infrastructure/services/MediaPickerService.ts +32 -8
  16. package/src/media/infrastructure/services/MediaSaveService.ts +7 -2
  17. package/src/media/presentation/hooks/useMedia.ts +60 -22
  18. package/src/molecules/BaseModal.tsx +1 -0
  19. package/src/molecules/ConfirmationModalMain.tsx +1 -0
  20. package/src/molecules/ListItem.tsx +15 -1
  21. package/src/molecules/avatar/Avatar.tsx +28 -11
  22. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -0
  23. package/src/molecules/calendar/presentation/components/AtomicCalendar.tsx +1 -1
  24. package/src/responsive/useResponsive.ts +1 -1
  25. package/src/services/api/ApiClient.ts +37 -6
  26. package/src/storage/presentation/hooks/usePersistentCache.ts +20 -12
  27. package/src/storage/presentation/hooks/useStore.ts +1 -0
  28. package/src/tanstack/presentation/hooks/usePrefetch.ts +14 -0
  29. package/src/theme/infrastructure/stores/themeStore.ts +13 -11
  30. package/src/timezone/infrastructure/services/BusinessCalendarManager.ts +1 -0
  31. package/src/timezone/infrastructure/services/CalendarManager.ts +2 -2
  32. package/src/timezone/infrastructure/services/DateComparisonUtils.ts +1 -0
  33. package/src/timezone/infrastructure/services/DateFormatter.ts +3 -2
  34. package/src/timezone/infrastructure/services/DateRangeUtils.ts +1 -0
  35. package/src/timezone/infrastructure/utils/TimezoneParsers.ts +27 -0
  36. package/src/utilities/sharing/presentation/hooks/useSharing.ts +44 -17
  37. package/src/utils/async/index.ts +12 -0
  38. package/src/utils/async/retryWithBackoff.ts +177 -0
  39. package/src/utils/errors/DesignSystemError.ts +117 -0
  40. package/src/utils/errors/ErrorHandler.ts +137 -0
  41. package/src/utils/errors/index.ts +7 -0
@@ -6,6 +6,7 @@
6
6
  import { File } from "expo-file-system";
7
7
  import type { FileEncoding, FileOperationResult } from "../../domain/entities/File";
8
8
  import { getEncodingType, type ExpoEncodingType } from "./encoding.service";
9
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
9
10
 
10
11
  /**
11
12
  * Write string to file
@@ -18,15 +19,19 @@ export async function writeFile(
18
19
  try {
19
20
  const encodingType = getEncodingType(encoding);
20
21
  const file = new File(uri);
21
- file.write(content, {
22
+ await file.write(content, {
22
23
  encoding: encodingType as ExpoEncodingType,
23
24
  });
24
25
  return { success: true, uri };
25
26
  } catch (error) {
26
- const writeError = error as Error;
27
+ const handled = ErrorHandler.handleAndLog(
28
+ error,
29
+ 'writeFile',
30
+ { uri, encoding, contentLength: content.length }
31
+ );
27
32
  return {
28
33
  success: false,
29
- error: writeError.message || "Unknown error",
34
+ error: handled.getUserMessage(),
30
35
  };
31
36
  }
32
37
  }
@@ -22,6 +22,7 @@ import {
22
22
  } from "../utils/mediaPickerMappers";
23
23
  import { PermissionManager } from "../utils/PermissionManager";
24
24
  import { FileValidator } from "../../domain/utils/FileValidator";
25
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
25
26
 
26
27
  /**
27
28
  * Media picker service for selecting images/videos
@@ -33,7 +34,11 @@ export class MediaPickerService {
33
34
  try {
34
35
  const permission = await PermissionManager.requestCameraPermission();
35
36
  if (!PermissionManager.isPermissionGranted(permission)) {
36
- return { canceled: true };
37
+ return {
38
+ canceled: true,
39
+ error: MediaValidationError.PERMISSION_DENIED,
40
+ errorMessage: "Camera permission was denied",
41
+ };
37
42
  }
38
43
 
39
44
  const result = await ImagePicker.launchCameraAsync({
@@ -45,8 +50,13 @@ export class MediaPickerService {
45
50
  });
46
51
 
47
52
  return mapPickerResult(result);
48
- } catch {
49
- return { canceled: true };
53
+ } catch (error) {
54
+ ErrorHandler.handleAndLog(error, 'launchCamera', { options });
55
+ return {
56
+ canceled: true,
57
+ error: MediaValidationError.PICKER_ERROR,
58
+ errorMessage: "Failed to launch camera",
59
+ };
50
60
  }
51
61
  }
52
62
 
@@ -56,7 +66,11 @@ export class MediaPickerService {
56
66
  try {
57
67
  const permission = await PermissionManager.requestCameraPermission();
58
68
  if (!PermissionManager.isPermissionGranted(permission)) {
59
- return { canceled: true };
69
+ return {
70
+ canceled: true,
71
+ error: MediaValidationError.PERMISSION_DENIED,
72
+ errorMessage: "Camera permission was denied",
73
+ };
60
74
  }
61
75
 
62
76
  const result = await ImagePicker.launchCameraAsync({
@@ -67,8 +81,13 @@ export class MediaPickerService {
67
81
  });
68
82
 
69
83
  return mapPickerResult(result);
70
- } catch {
71
- return { canceled: true };
84
+ } catch (error) {
85
+ ErrorHandler.handleAndLog(error, 'launchCameraForVideo', { options });
86
+ return {
87
+ canceled: true,
88
+ error: MediaValidationError.PICKER_ERROR,
89
+ errorMessage: "Failed to launch camera for video",
90
+ };
72
91
  }
73
92
  }
74
93
 
@@ -114,8 +133,13 @@ export class MediaPickerService {
114
133
  }
115
134
 
116
135
  return mappedResult;
117
- } catch {
118
- return { canceled: true };
136
+ } catch (error) {
137
+ ErrorHandler.handleAndLog(error, 'pickImage', { options });
138
+ return {
139
+ canceled: true,
140
+ error: MediaValidationError.PICKER_ERROR,
141
+ errorMessage: "Failed to pick image from library",
142
+ };
119
143
  }
120
144
  }
121
145
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as FileSystem from "expo-file-system";
7
7
  import { MediaLibraryPermission } from "../../domain/entities/Media";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  export interface SaveResult {
10
11
  success: boolean;
@@ -87,10 +88,14 @@ export class MediaSaveService {
87
88
  path: destination,
88
89
  };
89
90
  } catch (error) {
90
- const message = error instanceof Error ? error.message : "Unknown error";
91
+ const handled = ErrorHandler.handleAndLog(
92
+ error,
93
+ 'saveToStorage',
94
+ { uri, mediaType }
95
+ );
91
96
  return {
92
97
  success: false,
93
- error: `Failed to save media: ${message}`,
98
+ error: `Failed to save media: ${handled.getUserMessage()}`,
94
99
  };
95
100
  }
96
101
  }
@@ -5,7 +5,7 @@
5
5
  * Provides camera, gallery picking functionality.
6
6
  */
7
7
 
8
- import { useState, useCallback } from "react";
8
+ import { useState, useCallback, useEffect, useRef } from "react";
9
9
  import { MediaPickerService } from "../../infrastructure/services/MediaPickerService";
10
10
  import { PermissionManager } from "../../infrastructure/utils/PermissionManager";
11
11
  import type {
@@ -39,24 +39,38 @@ export const useMedia = () => {
39
39
  const [isLoading, setIsLoading] = useState(false);
40
40
  const [error, setError] = useState<string | null>(null);
41
41
 
42
+ // Track mounted state to prevent setState on unmounted component
43
+ const isMountedRef = useRef(true);
44
+ useEffect(() => {
45
+ return () => {
46
+ isMountedRef.current = false;
47
+ };
48
+ }, []);
49
+
42
50
  const pickImage = useCallback(
43
51
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
44
- setIsLoading(true);
45
- setError(null);
52
+ if (isMountedRef.current) {
53
+ setIsLoading(true);
54
+ setError(null);
55
+ }
46
56
  try {
47
57
  const result = await MediaPickerService.pickSingleImage(options);
48
58
  // Set error from validation result if present
49
- if (result.errorMessage) {
59
+ if (result.errorMessage && isMountedRef.current) {
50
60
  setError(result.errorMessage);
51
61
  }
52
62
  return result;
53
63
  } catch (err) {
54
64
  const errorMessage =
55
65
  err instanceof Error ? err.message : "Failed to pick image";
56
- setError(errorMessage);
66
+ if (isMountedRef.current) {
67
+ setError(errorMessage);
68
+ }
57
69
  return { canceled: true };
58
70
  } finally {
59
- setIsLoading(false);
71
+ if (isMountedRef.current) {
72
+ setIsLoading(false);
73
+ }
60
74
  }
61
75
  },
62
76
  []
@@ -64,18 +78,24 @@ export const useMedia = () => {
64
78
 
65
79
  const pickMultipleImages = useCallback(
66
80
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
67
- setIsLoading(true);
68
- setError(null);
81
+ if (isMountedRef.current) {
82
+ setIsLoading(true);
83
+ setError(null);
84
+ }
69
85
  try {
70
86
  const result = await MediaPickerService.pickMultipleImages(options);
71
87
  return result;
72
88
  } catch (err) {
73
89
  const errorMessage =
74
90
  err instanceof Error ? err.message : "Failed to pick images";
75
- setError(errorMessage);
91
+ if (isMountedRef.current) {
92
+ setError(errorMessage);
93
+ }
76
94
  return { canceled: true };
77
95
  } finally {
78
- setIsLoading(false);
96
+ if (isMountedRef.current) {
97
+ setIsLoading(false);
98
+ }
79
99
  }
80
100
  },
81
101
  []
@@ -83,18 +103,24 @@ export const useMedia = () => {
83
103
 
84
104
  const pickVideo = useCallback(
85
105
  async (options?: MediaPickerOptions): Promise<MediaPickerResult> => {
86
- setIsLoading(true);
87
- setError(null);
106
+ if (isMountedRef.current) {
107
+ setIsLoading(true);
108
+ setError(null);
109
+ }
88
110
  try {
89
111
  const result = await MediaPickerService.pickVideo(options);
90
112
  return result;
91
113
  } catch (err) {
92
114
  const errorMessage =
93
115
  err instanceof Error ? err.message : "Failed to pick video";
94
- setError(errorMessage);
116
+ if (isMountedRef.current) {
117
+ setError(errorMessage);
118
+ }
95
119
  return { canceled: true };
96
120
  } finally {
97
- setIsLoading(false);
121
+ if (isMountedRef.current) {
122
+ setIsLoading(false);
123
+ }
98
124
  }
99
125
  },
100
126
  []
@@ -102,18 +128,24 @@ export const useMedia = () => {
102
128
 
103
129
  const launchCamera = useCallback(
104
130
  async (options?: CameraOptions): Promise<MediaPickerResult> => {
105
- setIsLoading(true);
106
- setError(null);
131
+ if (isMountedRef.current) {
132
+ setIsLoading(true);
133
+ setError(null);
134
+ }
107
135
  try {
108
136
  const result = await MediaPickerService.launchCamera(options);
109
137
  return result;
110
138
  } catch (err) {
111
139
  const errorMessage =
112
140
  err instanceof Error ? err.message : "Failed to launch camera";
113
- setError(errorMessage);
141
+ if (isMountedRef.current) {
142
+ setError(errorMessage);
143
+ }
114
144
  return { canceled: true };
115
145
  } finally {
116
- setIsLoading(false);
146
+ if (isMountedRef.current) {
147
+ setIsLoading(false);
148
+ }
117
149
  }
118
150
  },
119
151
  []
@@ -121,18 +153,24 @@ export const useMedia = () => {
121
153
 
122
154
  const launchCameraForVideo = useCallback(
123
155
  async (options?: CameraOptions): Promise<MediaPickerResult> => {
124
- setIsLoading(true);
125
- setError(null);
156
+ if (isMountedRef.current) {
157
+ setIsLoading(true);
158
+ setError(null);
159
+ }
126
160
  try {
127
161
  const result = await MediaPickerService.launchCameraForVideo(options);
128
162
  return result;
129
163
  } catch (err) {
130
164
  const errorMessage =
131
165
  err instanceof Error ? err.message : "Failed to record video";
132
- setError(errorMessage);
166
+ if (isMountedRef.current) {
167
+ setError(errorMessage);
168
+ }
133
169
  return { canceled: true };
134
170
  } finally {
135
- setIsLoading(false);
171
+ if (isMountedRef.current) {
172
+ setIsLoading(false);
173
+ }
136
174
  }
137
175
  },
138
176
  []
@@ -51,6 +51,7 @@ export const BaseModal: React.FC<BaseModalProps> = ({
51
51
  onRequestClose={onClose}
52
52
  statusBarTranslucent
53
53
  testID={testID}
54
+ accessibilityViewIsModal={true}
54
55
  >
55
56
  <View style={styles.overlay}>
56
57
  <TouchableOpacity
@@ -64,6 +64,7 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
64
64
  onRequestClose={onCancel}
65
65
  statusBarTranslucent
66
66
  testID={testID}
67
+ accessibilityViewIsModal={true}
67
68
  >
68
69
  <View style={getModalOverlayStyle()}>
69
70
  <ConfirmationModalBackdrop
@@ -14,8 +14,22 @@ export const ListItem: React.FC<ListItemProps> = ({
14
14
  const listItemStyles = getListItemStyles(tokens);
15
15
  const Component = onPress ? TouchableOpacity : View;
16
16
 
17
+ const accessibilityProps = onPress
18
+ ? {
19
+ accessibilityRole: 'button' as const,
20
+ accessibilityLabel: title,
21
+ accessibilityState: { disabled },
22
+ }
23
+ : {};
24
+
17
25
  return (
18
- <Component style={[listItemStyles.container, disabled ? listItemStyles.disabled : undefined, style]} onPress={onPress} disabled={disabled} activeOpacity={0.7}>
26
+ <Component
27
+ style={[listItemStyles.container, disabled ? listItemStyles.disabled : undefined, style]}
28
+ onPress={onPress}
29
+ disabled={disabled}
30
+ activeOpacity={0.7}
31
+ {...accessibilityProps}
32
+ >
19
33
  {leftIcon && (
20
34
  <AtomicIcon
21
35
  name={leftIcon}
@@ -5,8 +5,8 @@
5
5
  * Handles loading states, fallbacks, and status indicators.
6
6
  */
7
7
 
8
- import React from 'react';
9
- import { View, Image, StyleSheet, type StyleProp, type ViewStyle, type ImageStyle } from 'react-native';
8
+ import React, { useMemo } from 'react';
9
+ import { View, Image, StyleSheet, TouchableOpacity, type StyleProp, type ViewStyle, type ImageStyle } from 'react-native';
10
10
  import { useAppDesignTokens } from '../../theme';
11
11
  import { AtomicText, AtomicIcon } from '../../atoms';
12
12
  import type { AvatarSize, AvatarShape } from './Avatar.types';
@@ -62,20 +62,29 @@ export const Avatar: React.FC<AvatarProps> = ({
62
62
  onPress,
63
63
  }) => {
64
64
  const tokens = useAppDesignTokens();
65
- const config = SIZE_CONFIGS[size];
65
+ const config = useMemo(() => SIZE_CONFIGS[size], [size]);
66
66
 
67
67
  // Determine avatar type and content
68
68
  const hasImage = !!uri;
69
69
  const hasName = !!name;
70
- const initials = hasName ? AvatarUtils.generateInitials(name) : AVATAR_CONSTANTS.FALLBACK_INITIALS;
71
- const bgColor = backgroundColor || (hasName ? AvatarUtils.getColorForName(name) : tokens.colors.surfaceSecondary);
72
- const borderRadius = AvatarUtils.getBorderRadius(shape, config.size);
70
+ const initials = useMemo(
71
+ () => (hasName ? AvatarUtils.generateInitials(name) : AVATAR_CONSTANTS.FALLBACK_INITIALS),
72
+ [hasName, name]
73
+ );
74
+ const bgColor = useMemo(
75
+ () => backgroundColor || (hasName ? AvatarUtils.getColorForName(name) : tokens.colors.surfaceSecondary),
76
+ [backgroundColor, hasName, name, tokens.colors.surfaceSecondary]
77
+ );
78
+ const borderRadius = useMemo(
79
+ () => AvatarUtils.getBorderRadius(shape, config.size),
80
+ [shape, config.size]
81
+ );
73
82
 
74
83
  // Status indicator position
75
- const statusPosition = {
84
+ const statusPosition = useMemo(() => ({
76
85
  bottom: 0,
77
86
  right: 0,
78
- };
87
+ }), []);
79
88
 
80
89
  const renderContent = () => {
81
90
  if (hasImage) {
@@ -122,8 +131,10 @@ export const Avatar: React.FC<AvatarProps> = ({
122
131
  );
123
132
  };
124
133
 
134
+ const AvatarWrapper = onPress ? TouchableOpacity : View;
135
+
125
136
  return (
126
- <View
137
+ <AvatarWrapper
127
138
  style={[
128
139
  styles.container,
129
140
  {
@@ -134,7 +145,11 @@ export const Avatar: React.FC<AvatarProps> = ({
134
145
  },
135
146
  style,
136
147
  ]}
137
- onTouchEnd={onPress}
148
+ onPress={onPress}
149
+ disabled={!onPress}
150
+ accessibilityRole={onPress ? 'button' : 'image'}
151
+ accessibilityLabel={name || 'User avatar'}
152
+ accessible={true}
138
153
  >
139
154
  {renderContent()}
140
155
 
@@ -153,9 +168,11 @@ export const Avatar: React.FC<AvatarProps> = ({
153
168
  ...statusPosition,
154
169
  },
155
170
  ]}
171
+ accessibilityLabel={`Status: ${status}`}
172
+ accessibilityRole="none"
156
173
  />
157
174
  )}
158
- </View>
175
+ </AvatarWrapper>
159
176
  );
160
177
  };
161
178
 
@@ -99,6 +99,7 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>((props,
99
99
  animationType="none"
100
100
  onRequestClose={dismiss}
101
101
  statusBarTranslucent
102
+ accessibilityViewIsModal={true}
102
103
  >
103
104
  <Pressable style={styles.overlay} onPress={dismiss}>
104
105
  <View style={styles.container}>
@@ -119,7 +119,7 @@ export const AtomicCalendar: React.FC<AtomicCalendarProps> = ({
119
119
 
120
120
  return (
121
121
  <CalendarDayCell
122
- key={index}
122
+ key={day.date.getTime()}
123
123
  day={day}
124
124
  index={index}
125
125
  isSelected={isSelected}
@@ -124,7 +124,7 @@ export const useResponsive = (): UseResponsiveReturn => {
124
124
  getGridCols,
125
125
  };
126
126
  },
127
- [width, height, getLogoSize, getInputHeight, getIconSize, getMaxWidth, getFontSize, getGridCols],
127
+ [width, height, insets, getLogoSize, getInputHeight, getIconSize, getMaxWidth, getFontSize, getGridCols],
128
128
  );
129
129
 
130
130
  return responsiveValues;
@@ -20,6 +20,8 @@ import {
20
20
  isSuccessfulResponse,
21
21
  fetchWithTimeout,
22
22
  } from './utils/responseHandler';
23
+ import { retryWithBackoff, isNetworkError, isRetryableHttpStatus } from '../../utils/async';
24
+ import { ErrorHandler } from '../../utils/errors';
23
25
 
24
26
  /**
25
27
  * Applies interceptors to a value
@@ -46,7 +48,7 @@ export class ApiClient {
46
48
  }
47
49
 
48
50
  /**
49
- * Makes an HTTP request
51
+ * Makes an HTTP request with automatic retry for retryable errors
50
52
  */
51
53
  async request<T>(requestConfig: ApiRequestConfig): Promise<ApiResponse<T>> {
52
54
  try {
@@ -61,11 +63,39 @@ export class ApiClient {
61
63
  headers: { ...this.config.headers, ...config.headers },
62
64
  });
63
65
 
64
- const response = await fetchWithTimeout(
65
- fullURL,
66
- fetchOptions,
67
- config.timeout || this.config.timeout || 30000
68
- );
66
+ // Retry only for GET requests and retryable errors
67
+ const shouldRetry = config.method === 'GET';
68
+ const timeout = config.timeout || this.config.timeout || 30000;
69
+
70
+ const response = shouldRetry
71
+ ? await retryWithBackoff(
72
+ () => fetchWithTimeout(fullURL, fetchOptions, timeout),
73
+ {
74
+ maxRetries: 3,
75
+ baseDelay: 1000,
76
+ shouldRetry: (error) => {
77
+ // Retry on network errors
78
+ if (isNetworkError(error as Error)) return true;
79
+
80
+ // Retry on specific HTTP status codes (5xx, 429, 408)
81
+ if ('status' in error && typeof error.status === 'number') {
82
+ return isRetryableHttpStatus(error.status);
83
+ }
84
+
85
+ return false;
86
+ },
87
+ onRetry: (error, attempt, delay) => {
88
+ if (__DEV__) {
89
+ ErrorHandler.log({
90
+ name: 'ApiRetry',
91
+ message: `Retrying API request (attempt ${attempt}) after ${delay}ms`,
92
+ context: { url: fullURL, error: error.message },
93
+ });
94
+ }
95
+ },
96
+ }
97
+ )
98
+ : await fetchWithTimeout(fullURL, fetchOptions, timeout);
69
99
 
70
100
  if (!isSuccessfulResponse(response)) {
71
101
  const error = await handleHttpError(response);
@@ -78,6 +108,7 @@ export class ApiClient {
78
108
  return parsedResponse;
79
109
  } catch (error) {
80
110
  const apiError = handleNetworkError(error);
111
+ ErrorHandler.log(apiError);
81
112
  throw await applyInterceptors(apiError, this.config.errorInterceptors);
82
113
  }
83
114
  }
@@ -99,16 +99,24 @@ export function usePersistentCache<T>(
99
99
  // Track if component is mounted to prevent state updates after unmount
100
100
  const isMountedRef = useRef(true);
101
101
 
102
+ // Stabilize actions to prevent circular dependency
103
+ const stableActionsRef = useRef(actions);
104
+ useEffect(() => {
105
+ stableActionsRef.current = actions;
106
+ });
107
+
102
108
  const loadFromStorage = useCallback(async () => {
109
+ const currentActions = stableActionsRef.current;
110
+
103
111
  if (!enabled) {
104
112
  if (isMountedRef.current) {
105
- actions.setLoading(false);
113
+ currentActions.setLoading(false);
106
114
  }
107
115
  return;
108
116
  }
109
117
 
110
118
  if (isMountedRef.current) {
111
- actions.setLoading(true);
119
+ currentActions.setLoading(true);
112
120
  }
113
121
 
114
122
  try {
@@ -117,35 +125,35 @@ export function usePersistentCache<T>(
117
125
  if (isMountedRef.current) {
118
126
  if (cached) {
119
127
  const expired = isCacheExpired(cached, version);
120
- actions.setData(cached.value);
121
- actions.setExpired(expired);
128
+ currentActions.setData(cached.value);
129
+ currentActions.setExpired(expired);
122
130
  } else {
123
- actions.clearData();
131
+ currentActions.clearData();
124
132
  }
125
133
  }
126
134
  } catch {
127
135
  if (isMountedRef.current) {
128
- actions.clearData();
136
+ currentActions.clearData();
129
137
  }
130
138
  } finally {
131
139
  if (isMountedRef.current) {
132
- actions.setLoading(false);
140
+ currentActions.setLoading(false);
133
141
  }
134
142
  }
135
- }, [key, version, enabled, actions, cacheOps]);
143
+ }, [key, version, enabled, cacheOps]); // Removed actions from dependencies
136
144
 
137
145
  const setData = useCallback(
138
146
  async (value: T) => {
139
147
  await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
140
- actions.setData(value);
148
+ stableActionsRef.current.setData(value);
141
149
  },
142
- [key, ttl, version, enabled, actions, cacheOps],
150
+ [key, ttl, version, enabled, cacheOps],
143
151
  );
144
152
 
145
153
  const clearData = useCallback(async () => {
146
154
  await cacheOps.clearFromStorage(key, enabled);
147
- actions.clearData();
148
- }, [key, enabled, actions, cacheOps]);
155
+ stableActionsRef.current.clearData();
156
+ }, [key, enabled, cacheOps]);
149
157
 
150
158
  const refresh = useCallback(async () => {
151
159
  await loadFromStorage();
@@ -16,6 +16,7 @@ export function useStore<T extends object>(config: StoreConfig<T>) {
16
16
  config.version,
17
17
  config.persist,
18
18
  config.storage,
19
+ JSON.stringify(config.initialState), // Track initialState changes
19
20
  ]
20
21
  );
21
22
 
@@ -30,6 +30,13 @@ export function usePrefetchQuery<
30
30
  const queryClient = useQueryClient();
31
31
  const prefetchingRef = useRef(new Set<TVariables>());
32
32
 
33
+ // Cleanup on unmount
34
+ useEffect(() => {
35
+ return () => {
36
+ prefetchingRef.current.clear();
37
+ };
38
+ }, []);
39
+
33
40
  return useCallback(
34
41
  async (variables: TVariables) => {
35
42
  if (prefetchingRef.current.has(variables)) return;
@@ -62,6 +69,13 @@ export function usePrefetchInfiniteQuery<
62
69
  const queryClient = useQueryClient();
63
70
  const hasPrefetchedRef = useRef(false);
64
71
 
72
+ // Cleanup on unmount
73
+ useEffect(() => {
74
+ return () => {
75
+ hasPrefetchedRef.current = false;
76
+ };
77
+ }, []);
78
+
65
79
  return useCallback(async () => {
66
80
  if (hasPrefetchedRef.current) return;
67
81