@umituz/react-native-storage 2.0.0 → 2.3.2

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 (34) hide show
  1. package/package.json +23 -7
  2. package/src/__tests__/integration.test.ts +391 -0
  3. package/src/__tests__/mocks/asyncStorage.mock.ts +52 -0
  4. package/src/__tests__/performance.test.ts +351 -0
  5. package/src/__tests__/setup.ts +63 -0
  6. package/src/application/ports/IStorageRepository.ts +0 -12
  7. package/src/domain/entities/StorageResult.ts +1 -3
  8. package/src/domain/entities/__tests__/CachedValue.test.ts +149 -0
  9. package/src/domain/entities/__tests__/StorageResult.test.ts +122 -0
  10. package/src/domain/errors/StorageError.ts +0 -2
  11. package/src/domain/errors/__tests__/StorageError.test.ts +127 -0
  12. package/src/domain/utils/__tests__/devUtils.test.ts +97 -0
  13. package/src/domain/utils/devUtils.ts +37 -0
  14. package/src/domain/value-objects/StorageKey.ts +27 -29
  15. package/src/index.ts +9 -1
  16. package/src/infrastructure/adapters/StorageService.ts +8 -6
  17. package/src/infrastructure/repositories/AsyncStorageRepository.ts +27 -108
  18. package/src/infrastructure/repositories/BaseStorageOperations.ts +101 -0
  19. package/src/infrastructure/repositories/BatchStorageOperations.ts +42 -0
  20. package/src/infrastructure/repositories/StringStorageOperations.ts +44 -0
  21. package/src/infrastructure/repositories/__tests__/AsyncStorageRepository.test.ts +169 -0
  22. package/src/infrastructure/repositories/__tests__/BaseStorageOperations.test.ts +200 -0
  23. package/src/presentation/hooks/CacheStorageOperations.ts +95 -0
  24. package/src/presentation/hooks/__tests__/usePersistentCache.test.ts +404 -0
  25. package/src/presentation/hooks/__tests__/useStorage.test.ts +246 -0
  26. package/src/presentation/hooks/__tests__/useStorageState.test.ts +292 -0
  27. package/src/presentation/hooks/useCacheState.ts +55 -0
  28. package/src/presentation/hooks/usePersistentCache.ts +30 -39
  29. package/src/presentation/hooks/useStorage.ts +4 -3
  30. package/src/presentation/hooks/useStorageState.ts +24 -8
  31. package/src/presentation/hooks/useStore.ts +3 -1
  32. package/src/types/global.d.ts +40 -0
  33. package/LICENSE +0 -22
  34. package/src/presentation/hooks/usePersistedState.ts +0 -34
@@ -0,0 +1,246 @@
1
+ /**
2
+ * useStorage Hook Tests
3
+ *
4
+ * Unit tests for useStorage hook
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { useStorage } from '../useStorage';
9
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
10
+
11
+ describe('useStorage Hook', () => {
12
+ beforeEach(() => {
13
+ (AsyncStorage as any).__clear();
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ describe('getItem', () => {
18
+ it('should get item from storage', async () => {
19
+ const key = 'test-key';
20
+ const defaultValue = 'default';
21
+ const expectedValue = 'test-value';
22
+
23
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
24
+
25
+ const { result } = renderHook(() => useStorage());
26
+
27
+ const value = await result.current.getItem(key, defaultValue);
28
+
29
+ expect(value).toBe(expectedValue);
30
+ });
31
+
32
+ it('should return default value for missing key', async () => {
33
+ const key = 'missing-key';
34
+ const defaultValue = 'default';
35
+
36
+ const { result } = renderHook(() => useStorage());
37
+
38
+ const value = await result.current.getItem(key, defaultValue);
39
+
40
+ expect(value).toBe(defaultValue);
41
+ });
42
+
43
+ it('should handle StorageKey enum', async () => {
44
+ const key = '@ui_preferences';
45
+ const defaultValue = 'default';
46
+ const expectedValue = 'test-value';
47
+
48
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
49
+
50
+ const { result } = renderHook(() => useStorage());
51
+
52
+ const value = await result.current.getItem(key as any, defaultValue);
53
+
54
+ expect(value).toBe(expectedValue);
55
+ });
56
+ });
57
+
58
+ describe('setItem', () => {
59
+ it('should set item to storage', async () => {
60
+ const key = 'test-key';
61
+ const value = 'test-value';
62
+
63
+ const { result } = renderHook(() => useStorage());
64
+
65
+ const success = await result.current.setItem(key, value);
66
+
67
+ expect(success).toBe(true);
68
+
69
+ // Verify storage
70
+ const stored = await AsyncStorage.getItem(key);
71
+ expect(JSON.parse(stored!)).toBe(value);
72
+ });
73
+
74
+ it('should return false on failure', async () => {
75
+ const key = 'test-key';
76
+ const value = { circular: {} };
77
+ value.circular = value; // Create circular reference
78
+
79
+ const { result } = renderHook(() => useStorage());
80
+
81
+ const success = await result.current.setItem(key, value);
82
+
83
+ expect(success).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('getString', () => {
88
+ it('should get string from storage', async () => {
89
+ const key = 'test-key';
90
+ const defaultValue = 'default';
91
+ const expectedValue = 'test-string';
92
+
93
+ await AsyncStorage.setItem(key, expectedValue);
94
+
95
+ const { result } = renderHook(() => useStorage());
96
+
97
+ const value = await result.current.getString(key, defaultValue);
98
+
99
+ expect(value).toBe(expectedValue);
100
+ });
101
+ });
102
+
103
+ describe('setString', () => {
104
+ it('should set string to storage', async () => {
105
+ const key = 'test-key';
106
+ const value = 'test-string';
107
+
108
+ const { result } = renderHook(() => useStorage());
109
+
110
+ const success = await result.current.setString(key, value);
111
+
112
+ expect(success).toBe(true);
113
+
114
+ // Verify storage
115
+ const stored = await AsyncStorage.getItem(key);
116
+ expect(stored).toBe(value);
117
+ });
118
+ });
119
+
120
+ describe('removeItem', () => {
121
+ it('should remove item from storage', async () => {
122
+ const key = 'test-key';
123
+
124
+ // Setup
125
+ await AsyncStorage.setItem(key, 'test-value');
126
+
127
+ const { result } = renderHook(() => useStorage());
128
+
129
+ const success = await result.current.removeItem(key);
130
+
131
+ expect(success).toBe(true);
132
+
133
+ // Verify removal
134
+ const stored = await AsyncStorage.getItem(key);
135
+ expect(stored).toBeNull();
136
+ });
137
+ });
138
+
139
+ describe('hasItem', () => {
140
+ it('should return true for existing item', async () => {
141
+ const key = 'test-key';
142
+
143
+ await AsyncStorage.setItem(key, 'test-value');
144
+
145
+ const { result } = renderHook(() => useStorage());
146
+
147
+ const exists = await result.current.hasItem(key);
148
+
149
+ expect(exists).toBe(true);
150
+ });
151
+
152
+ it('should return false for missing item', async () => {
153
+ const key = 'missing-key';
154
+
155
+ const { result } = renderHook(() => useStorage());
156
+
157
+ const exists = await result.current.hasItem(key);
158
+
159
+ expect(exists).toBe(false);
160
+ });
161
+ });
162
+
163
+ describe('clearAll', () => {
164
+ it('should clear all storage', async () => {
165
+ // Setup
166
+ await AsyncStorage.setItem('key1', 'value1');
167
+ await AsyncStorage.setItem('key2', 'value2');
168
+
169
+ const { result } = renderHook(() => useStorage());
170
+
171
+ const success = await result.current.clearAll();
172
+
173
+ expect(success).toBe(true);
174
+
175
+ // Verify clear
176
+ const keys = await AsyncStorage.getAllKeys();
177
+ expect(keys).toHaveLength(0);
178
+ });
179
+ });
180
+
181
+ describe('getItemWithResult', () => {
182
+ it('should return full result object', async () => {
183
+ const key = 'test-key';
184
+ const defaultValue = 'default';
185
+ const expectedValue = 'test-value';
186
+
187
+ await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
188
+
189
+ const { result } = renderHook(() => useStorage());
190
+
191
+ const storageResult = await result.current.getItemWithResult(key, defaultValue);
192
+
193
+ expect(storageResult.success).toBe(true);
194
+ expect(storageResult.data).toBe(expectedValue);
195
+ expect(storageResult.error).toBeUndefined();
196
+ });
197
+ });
198
+
199
+ describe('Performance', () => {
200
+ it('should memoize return object', () => {
201
+ const { result, rerender } = renderHook(() => useStorage());
202
+
203
+ const firstCall = result.current;
204
+ rerender();
205
+ const secondCall = result.current;
206
+
207
+ // Should be the same reference (memoized)
208
+ expect(firstCall).toBe(secondCall);
209
+ });
210
+
211
+ it('should not recreate functions on re-render', () => {
212
+ const { result, rerender } = renderHook(() => useStorage());
213
+
214
+ const firstFunctions = {
215
+ getItem: result.current.getItem,
216
+ setItem: result.current.setItem,
217
+ getString: result.current.getString,
218
+ setString: result.current.setString,
219
+ removeItem: result.current.removeItem,
220
+ hasItem: result.current.hasItem,
221
+ clearAll: result.current.clearAll,
222
+ getItemWithResult: result.current.getItemWithResult,
223
+ };
224
+
225
+ rerender();
226
+
227
+ const secondFunctions = {
228
+ getItem: result.current.getItem,
229
+ setItem: result.current.setItem,
230
+ getString: result.current.getString,
231
+ setString: result.current.setString,
232
+ removeItem: result.current.removeItem,
233
+ hasItem: result.current.hasItem,
234
+ clearAll: result.current.clearAll,
235
+ getItemWithResult: result.current.getItemWithResult,
236
+ };
237
+
238
+ // All functions should be the same reference
239
+ Object.keys(firstFunctions).forEach(key => {
240
+ expect(firstFunctions[key as keyof typeof firstFunctions]).toBe(
241
+ secondFunctions[key as keyof typeof secondFunctions]
242
+ );
243
+ });
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,292 @@
1
+ /**
2
+ * useStorageState Hook Tests
3
+ *
4
+ * Unit tests for useStorageState hook
5
+ */
6
+
7
+ import { renderHook, act } from '@testing-library/react-hooks';
8
+ import { useStorageState } from '../useStorageState';
9
+ import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
10
+
11
+ describe('useStorageState Hook', () => {
12
+ beforeEach(() => {
13
+ (AsyncStorage as any).__clear();
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ describe('Initial Load', () => {
18
+ it('should load initial value from storage', async () => {
19
+ const key = 'test-key';
20
+ const defaultValue = 'default';
21
+ const storedValue = 'stored-value';
22
+
23
+ await AsyncStorage.setItem(key, JSON.stringify(storedValue));
24
+
25
+ const { result, waitForNextUpdate } = renderHook(() =>
26
+ useStorageState(key, defaultValue)
27
+ );
28
+
29
+ // Initial state
30
+ expect(result.current[0]).toBe(defaultValue);
31
+ expect(result.current[2]).toBe(true); // isLoading
32
+
33
+ // Wait for load
34
+ await act(async () => {
35
+ await waitForNextUpdate();
36
+ });
37
+
38
+ expect(result.current[0]).toBe(storedValue);
39
+ expect(result.current[2]).toBe(false); // isLoading
40
+ });
41
+
42
+ it('should use default value for missing key', async () => {
43
+ const key = 'missing-key';
44
+ const defaultValue = 'default';
45
+
46
+ const { result, waitForNextUpdate } = renderHook(() =>
47
+ useStorageState(key, defaultValue)
48
+ );
49
+
50
+ await act(async () => {
51
+ await waitForNextUpdate();
52
+ });
53
+
54
+ expect(result.current[0]).toBe(defaultValue);
55
+ expect(result.current[2]).toBe(false);
56
+ });
57
+
58
+ it('should handle StorageKey enum', async () => {
59
+ const key = '@ui_preferences';
60
+ const defaultValue = 'default';
61
+ const storedValue = 'stored-value';
62
+
63
+ await AsyncStorage.setItem(key, JSON.stringify(storedValue));
64
+
65
+ const { result, waitForNextUpdate } = renderHook(() =>
66
+ useStorageState(key as any, defaultValue)
67
+ );
68
+
69
+ await act(async () => {
70
+ await waitForNextUpdate();
71
+ });
72
+
73
+ expect(result.current[0]).toBe(storedValue);
74
+ });
75
+ });
76
+
77
+ describe('Update State', () => {
78
+ it('should update state and persist to storage', async () => {
79
+ const key = 'test-key';
80
+ const defaultValue = 'default';
81
+ const newValue = 'new-value';
82
+
83
+ const { result, waitForNextUpdate } = renderHook(() =>
84
+ useStorageState(key, defaultValue)
85
+ );
86
+
87
+ await act(async () => {
88
+ await waitForNextUpdate();
89
+ });
90
+
91
+ // Update state
92
+ await act(async () => {
93
+ await result.current[1](newValue);
94
+ });
95
+
96
+ expect(result.current[0]).toBe(newValue);
97
+
98
+ // Verify storage
99
+ const stored = await AsyncStorage.getItem(key);
100
+ expect(JSON.parse(stored!)).toBe(newValue);
101
+ });
102
+
103
+ it('should handle update during loading', async () => {
104
+ const key = 'test-key';
105
+ const defaultValue = 'default';
106
+ const newValue = 'new-value';
107
+
108
+ // Mock slow storage
109
+ let resolveStorage: (value: string) => void;
110
+ (AsyncStorage.getItem as jest.Mock).mockImplementation(() =>
111
+ new Promise(resolve => {
112
+ resolveStorage = resolve;
113
+ })
114
+ );
115
+
116
+ const { result } = renderHook(() =>
117
+ useStorageState(key, defaultValue)
118
+ );
119
+
120
+ // Update while loading
121
+ await act(async () => {
122
+ await result.current[1](newValue);
123
+ });
124
+
125
+ expect(result.current[0]).toBe(newValue);
126
+ });
127
+ });
128
+
129
+ describe('Memory Leak Prevention', () => {
130
+ it('should cleanup on unmount', async () => {
131
+ const key = 'test-key';
132
+ const defaultValue = 'default';
133
+
134
+ // Mock slow storage
135
+ let resolveStorage: (value: string) => void;
136
+ (AsyncStorage.getItem as jest.Mock).mockImplementation(() =>
137
+ new Promise(resolve => {
138
+ resolveStorage = resolve;
139
+ })
140
+ );
141
+
142
+ const { result, unmount } = renderHook(() =>
143
+ useStorageState(key, defaultValue)
144
+ );
145
+
146
+ // Unmount before load completes
147
+ unmount();
148
+
149
+ // Resolve storage after unmount
150
+ await act(async () => {
151
+ resolveStorage!('stored-value');
152
+ });
153
+
154
+ // State should not be updated after unmount
155
+ expect(result.current[0]).toBe(defaultValue);
156
+ });
157
+
158
+ it('should handle unmount during update', async () => {
159
+ const key = 'test-key';
160
+ const defaultValue = 'default';
161
+ const newValue = 'new-value';
162
+
163
+ // Mock slow storage
164
+ let resolveStorage: () => void;
165
+ (AsyncStorage.setItem as jest.Mock).mockImplementation(() =>
166
+ new Promise(resolve => {
167
+ resolveStorage = resolve;
168
+ })
169
+ );
170
+
171
+ const { result, unmount } = renderHook(() =>
172
+ useStorageState(key, defaultValue)
173
+ );
174
+
175
+ // Start update
176
+ const updatePromise = act(async () => {
177
+ await result.current[1](newValue);
178
+ });
179
+
180
+ // Unmount before update completes
181
+ unmount();
182
+
183
+ // Complete update
184
+ await act(async () => {
185
+ resolveStorage!();
186
+ });
187
+
188
+ await updatePromise;
189
+ });
190
+ });
191
+
192
+ describe('Performance', () => {
193
+ it('should not re-run effect when defaultValue changes', async () => {
194
+ const key = 'test-key';
195
+ const defaultValue1 = 'default1';
196
+ const defaultValue2 = 'default2';
197
+
198
+ await AsyncStorage.setItem(key, JSON.stringify('stored-value'));
199
+
200
+ const { result, rerender, waitForNextUpdate } = renderHook(
201
+ ({ defaultValue }) => useStorageState(key, defaultValue),
202
+ { initialProps: { defaultValue: defaultValue1 } }
203
+ );
204
+
205
+ await act(async () => {
206
+ await waitForNextUpdate();
207
+ });
208
+
209
+ const getItemCalls = (AsyncStorage.getItem as jest.Mock).mock.calls.length;
210
+
211
+ // Rerender with different default value
212
+ rerender({ defaultValue: defaultValue2 });
213
+
214
+ // Should not trigger another storage read
215
+ expect((AsyncStorage.getItem as jest.Mock).mock.calls.length).toBe(getItemCalls);
216
+ });
217
+
218
+ it('should re-run effect when key changes', async () => {
219
+ const key1 = 'key1';
220
+ const key2 = 'key2';
221
+ const defaultValue = 'default';
222
+
223
+ await AsyncStorage.setItem(key1, JSON.stringify('value1'));
224
+ await AsyncStorage.setItem(key2, JSON.stringify('value2'));
225
+
226
+ const { result, rerender, waitForNextUpdate } = renderHook(
227
+ ({ key }) => useStorageState(key, defaultValue),
228
+ { initialProps: { key: key1 } }
229
+ );
230
+
231
+ await act(async () => {
232
+ await waitForNextUpdate();
233
+ });
234
+
235
+ expect(result.current[0]).toBe('value1');
236
+
237
+ // Change key
238
+ rerender({ key: key2 });
239
+
240
+ await act(async () => {
241
+ await waitForNextUpdate();
242
+ });
243
+
244
+ expect(result.current[0]).toBe('value2');
245
+ });
246
+ });
247
+
248
+ describe('Error Handling', () => {
249
+ it('should handle storage read error', async () => {
250
+ const key = 'test-key';
251
+ const defaultValue = 'default';
252
+
253
+ // Mock storage error
254
+ (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
255
+
256
+ const { result, waitForNextUpdate } = renderHook(() =>
257
+ useStorageState(key, defaultValue)
258
+ );
259
+
260
+ await act(async () => {
261
+ await waitForNextUpdate();
262
+ });
263
+
264
+ expect(result.current[0]).toBe(defaultValue);
265
+ expect(result.current[2]).toBe(false); // isLoading should be false
266
+ });
267
+
268
+ it('should handle storage write error', async () => {
269
+ const key = 'test-key';
270
+ const defaultValue = 'default';
271
+ const newValue = 'new-value';
272
+
273
+ // Mock storage error
274
+ (AsyncStorage.setItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
275
+
276
+ const { result, waitForNextUpdate } = renderHook(() =>
277
+ useStorageState(key, defaultValue)
278
+ );
279
+
280
+ await act(async () => {
281
+ await waitForNextUpdate();
282
+ });
283
+
284
+ // State should still update even if storage fails
285
+ await act(async () => {
286
+ await result.current[1](newValue);
287
+ });
288
+
289
+ expect(result.current[0]).toBe(newValue);
290
+ });
291
+ });
292
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Cache State Manager
3
+ *
4
+ * Manages cache state following Single Responsibility Principle
5
+ */
6
+
7
+ import { useState, useCallback, useMemo } from 'react';
8
+ import type { CachedValue } from '../../domain/entities/CachedValue';
9
+ import { createCachedValue, isCacheExpired } from '../../domain/entities/CachedValue';
10
+
11
+ export interface CacheState<T> {
12
+ data: T | null;
13
+ isLoading: boolean;
14
+ isExpired: boolean;
15
+ }
16
+
17
+ export interface CacheActions<T> {
18
+ setData: (value: T) => void;
19
+ clearData: () => void;
20
+ setLoading: (loading: boolean) => void;
21
+ setExpired: (expired: boolean) => void;
22
+ }
23
+
24
+ /**
25
+ * Hook for managing cache state
26
+ */
27
+ export function useCacheState<T>(): [CacheState<T>, CacheActions<T>] {
28
+ const [data, setDataState] = useState<T | null>(null);
29
+ const [isLoading, setIsLoading] = useState(true);
30
+ const [isExpired, setIsExpired] = useState(false);
31
+
32
+ const setData = useCallback((value: T) => {
33
+ setDataState(value);
34
+ setIsExpired(false);
35
+ }, []);
36
+
37
+ const clearData = useCallback(() => {
38
+ setDataState(null);
39
+ setIsExpired(true);
40
+ }, []);
41
+
42
+ const setLoading = useCallback((loading: boolean) => {
43
+ setIsLoading(loading);
44
+ }, []);
45
+
46
+ const setExpired = useCallback((expired: boolean) => {
47
+ setIsExpired(expired);
48
+ }, []);
49
+
50
+ // Memoize state and actions objects for performance
51
+ const state: CacheState<T> = useMemo(() => ({ data, isLoading, isExpired }), [data, isLoading, isExpired]);
52
+ const actions: CacheActions<T> = useMemo(() => ({ setData, clearData, setLoading, setExpired }), [setData, clearData, setLoading, setExpired]);
53
+
54
+ return [state, actions];
55
+ }
@@ -5,10 +5,10 @@
5
5
  * General-purpose cache hook for any app
6
6
  */
7
7
 
8
- import { useState, useEffect, useCallback } from 'react';
9
- import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
10
- import type { CachedValue } from '../../domain/entities/CachedValue';
11
- import { createCachedValue, isCacheExpired } from '../../domain/entities/CachedValue';
8
+ import { useEffect, useCallback, useMemo } from 'react';
9
+ import { useCacheState } from './useCacheState';
10
+ import { CacheStorageOperations } from './CacheStorageOperations';
11
+ import { isCacheExpired } from '../../domain/entities/CachedValue';
12
12
  import { DEFAULT_TTL } from '../../domain/constants/CacheDefaults';
13
13
 
14
14
  /**
@@ -91,71 +91,62 @@ export function usePersistentCache<T>(
91
91
  options: PersistentCacheOptions = {},
92
92
  ): PersistentCacheResult<T> {
93
93
  const { ttl = DEFAULT_TTL.MEDIUM, version, enabled = true } = options;
94
-
95
- const [data, setDataState] = useState<T | null>(null);
96
- const [isLoading, setIsLoading] = useState(true);
97
- const [isExpired, setIsExpired] = useState(false);
94
+ const [state, actions] = useCacheState<T>();
95
+
96
+ // Use singleton pattern to prevent memory leaks
97
+ const cacheOps = useMemo(() => CacheStorageOperations.getInstance(), []);
98
98
 
99
99
  const loadFromStorage = useCallback(async () => {
100
100
  if (!enabled) {
101
- setIsLoading(false);
101
+ actions.setLoading(false);
102
102
  return;
103
103
  }
104
104
 
105
+ actions.setLoading(true);
106
+
105
107
  try {
106
- const result = await storageRepository.getString(key, '');
108
+ const cached = await cacheOps.loadFromStorage<T>(key, version);
107
109
 
108
- if (result.success && result.data) {
109
- const cached = JSON.parse(result.data) as CachedValue<T>;
110
+ if (cached) {
110
111
  const expired = isCacheExpired(cached, version);
111
-
112
- setDataState(cached.value);
113
- setIsExpired(expired);
112
+ actions.setData(cached.value);
113
+ actions.setExpired(expired);
114
114
  } else {
115
- setDataState(null);
116
- setIsExpired(true);
115
+ actions.clearData();
117
116
  }
118
117
  } catch {
119
- setDataState(null);
120
- setIsExpired(true);
118
+ actions.clearData();
121
119
  } finally {
122
- setIsLoading(false);
120
+ actions.setLoading(false);
123
121
  }
124
- }, [key, version, enabled]);
122
+ }, [key, version, enabled, actions, cacheOps]);
125
123
 
126
124
  const setData = useCallback(
127
125
  async (value: T) => {
128
- if (!enabled) return;
129
-
130
- const cached = createCachedValue(value, ttl, version);
131
- await storageRepository.setString(key, JSON.stringify(cached));
132
- setDataState(value);
133
- setIsExpired(false);
126
+ await cacheOps.saveToStorage(key, value, { ttl, version, enabled });
127
+ actions.setData(value);
134
128
  },
135
- [key, ttl, version, enabled],
129
+ [key, ttl, version, enabled, actions, cacheOps],
136
130
  );
137
131
 
138
132
  const clearData = useCallback(async () => {
139
- if (!enabled) return;
140
-
141
- await storageRepository.removeItem(key);
142
- setDataState(null);
143
- setIsExpired(true);
144
- }, [key, enabled]);
133
+ await cacheOps.clearFromStorage(key, enabled);
134
+ actions.clearData();
135
+ }, [key, enabled, actions, cacheOps]);
145
136
 
146
137
  const refresh = useCallback(async () => {
147
- setIsLoading(true);
148
138
  await loadFromStorage();
149
139
  }, [loadFromStorage]);
150
140
 
141
+ // Prevent infinite loops by only running when key or enabled changes
151
142
  useEffect(() => {
152
143
  loadFromStorage();
153
- }, [loadFromStorage]);
144
+ }, [key, enabled]);
154
145
 
155
146
  return {
156
- data,
157
- isLoading,
158
- isExpired,
147
+ data: state.data,
148
+ isLoading: state.isLoading,
149
+ isExpired: state.isExpired,
159
150
  setData,
160
151
  clearData,
161
152
  refresh,