@planningcenter/chat-react-native 3.20.1-rc.0 → 3.20.1-rc.1

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.
@@ -1,4 +1,4 @@
1
1
  type SetValue<TCacheData> = (_itemValue?: TCacheData | null) => Promise<void>;
2
- export declare function useAsyncStorage<TCacheData>(key: string, initialValue: TCacheData): [TCacheData, SetValue<TCacheData>];
2
+ export declare function useAsyncStorage<TCacheData>(key: string, initialValue: TCacheData, throwOnError?: boolean): [TCacheData, SetValue<TCacheData>];
3
3
  export {};
4
4
  //# sourceMappingURL=use_async_storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use_async_storage.d.ts","sourceRoot":"","sources":["../../src/hooks/use_async_storage.ts"],"names":[],"mappings":"AAMA,KAAK,QAAQ,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE7E,wBAAgB,eAAe,CAAC,UAAU,EACxC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,UAAU,GACvB,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAgCpC"}
1
+ {"version":3,"file":"use_async_storage.d.ts","sourceRoot":"","sources":["../../src/hooks/use_async_storage.ts"],"names":[],"mappings":"AAOA,KAAK,QAAQ,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE7E,wBAAgB,eAAe,CAAC,UAAU,EACxC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,UAAU,EACxB,YAAY,GAAE,OAAe,GAC5B,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CA+CpC"}
@@ -1,12 +1,14 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
2
  import { useSuspenseQuery } from '@tanstack/react-query';
3
+ import { noop } from 'lodash';
3
4
  import { useCallback } from 'react';
4
5
  const cacheKeyGenerator = (key) => [`AsyncStorageResource:${key}`];
5
- export function useAsyncStorage(key, initialValue) {
6
+ export function useAsyncStorage(key, initialValue, throwOnError = false) {
6
7
  const cacheKey = cacheKeyGenerator(key);
7
8
  const { data: value, refetch } = useSuspenseQuery({
8
9
  queryKey: cacheKey,
9
- queryFn: () => AsyncStorage.getItem(key).then(storedValue => {
10
+ queryFn: () => AsyncStorage.getItem(key)
11
+ .then(storedValue => {
10
12
  if (!storedValue)
11
13
  return initialValue;
12
14
  try {
@@ -15,18 +17,31 @@ export function useAsyncStorage(key, initialValue) {
15
17
  catch {
16
18
  return storedValue;
17
19
  }
20
+ })
21
+ .catch(e => {
22
+ if (!throwOnError)
23
+ return initialValue;
24
+ return Promise.reject(e);
18
25
  }),
19
26
  });
20
27
  const setValue = useCallback(itemValue => {
21
28
  if (itemValue === null || itemValue === undefined) {
22
- return AsyncStorage.removeItem(key).then(() => {
29
+ return AsyncStorage.removeItem(key)
30
+ .then(() => {
23
31
  refetch();
24
- });
32
+ })
33
+ .catch(noop);
25
34
  }
26
- return AsyncStorage.setItem(key, JSON.stringify(itemValue)).then(() => {
35
+ return AsyncStorage.setItem(key, JSON.stringify(itemValue))
36
+ .then(() => {
27
37
  refetch();
38
+ })
39
+ .catch(e => {
40
+ if (!throwOnError)
41
+ return;
42
+ return Promise.reject(e);
28
43
  });
29
- }, [key, refetch]);
44
+ }, [throwOnError, key, refetch]);
30
45
  return [value || initialValue, setValue];
31
46
  }
32
47
  //# sourceMappingURL=use_async_storage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use_async_storage.js","sourceRoot":"","sources":["../../src/hooks/use_async_storage.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,2CAA2C,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,wBAAwB,GAAG,EAAE,CAAC,CAAA;AAI1E,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,YAAwB;IAExB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAa;QAC5D,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,GAAG,EAAE,CACZ,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE;YAC3C,IAAI,CAAC,WAAW;gBAAE,OAAO,YAAY,CAAA;YAErC,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,WAAW,CAAA;YACpB,CAAC;QACH,CAAC,CAAC;KACL,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAyB,WAAW,CAChD,SAAS,CAAC,EAAE;QACV,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAClD,OAAO,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;gBAC5C,OAAO,EAAE,CAAA;YACX,CAAC,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACpE,OAAO,EAAE,CAAA;QACX,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,CAAC,GAAG,EAAE,OAAO,CAAC,CACf,CAAA;IAED,OAAO,CAAC,KAAK,IAAI,YAAY,EAAE,QAAQ,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { useSuspenseQuery } from '@tanstack/react-query'\nimport { useCallback } from 'react'\n\nconst cacheKeyGenerator = (key: string) => [`AsyncStorageResource:${key}`]\n\ntype SetValue<TCacheData> = (_itemValue?: TCacheData | null) => Promise<void>\n\nexport function useAsyncStorage<TCacheData>(\n key: string,\n initialValue: TCacheData\n): [TCacheData, SetValue<TCacheData>] {\n const cacheKey = cacheKeyGenerator(key)\n const { data: value, refetch } = useSuspenseQuery<TCacheData>({\n queryKey: cacheKey,\n queryFn: () =>\n AsyncStorage.getItem(key).then(storedValue => {\n if (!storedValue) return initialValue\n\n try {\n return JSON.parse(storedValue)\n } catch {\n return storedValue\n }\n }),\n })\n\n const setValue: SetValue<TCacheData> = useCallback(\n itemValue => {\n if (itemValue === null || itemValue === undefined) {\n return AsyncStorage.removeItem(key).then(() => {\n refetch()\n })\n }\n\n return AsyncStorage.setItem(key, JSON.stringify(itemValue)).then(() => {\n refetch()\n })\n },\n [key, refetch]\n )\n\n return [value || initialValue, setValue]\n}\n"]}
1
+ {"version":3,"file":"use_async_storage.js","sourceRoot":"","sources":["../../src/hooks/use_async_storage.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,2CAA2C,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,wBAAwB,GAAG,EAAE,CAAC,CAAA;AAI1E,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,YAAwB,EACxB,eAAwB,KAAK;IAE7B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IAEvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAa;QAC5D,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,GAAG,EAAE,CACZ,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;aACtB,IAAI,CAAC,WAAW,CAAC,EAAE;YAClB,IAAI,CAAC,WAAW;gBAAE,OAAO,YAAY,CAAA;YAErC,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,WAAW,CAAA;YACpB,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,CAAC,EAAE;YACT,IAAI,CAAC,YAAY;gBAAE,OAAO,YAAY,CAAA;YAEtC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC,CAAC;KACP,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAyB,WAAW,CAChD,SAAS,CAAC,EAAE;QACV,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAClD,OAAO,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;iBAChC,IAAI,CAAC,GAAG,EAAE;gBACT,OAAO,EAAE,CAAA;YACX,CAAC,CAAC;iBACD,KAAK,CAAC,IAAI,CAAC,CAAA;QAChB,CAAC;QAED,OAAO,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;aACxD,IAAI,CAAC,GAAG,EAAE;YACT,OAAO,EAAE,CAAA;QACX,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,CAAC,EAAE;YACT,IAAI,CAAC,YAAY;gBAAE,OAAM;YAEzB,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC1B,CAAC,CAAC,CAAA;IACN,CAAC,EACD,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,CAC7B,CAAA;IAED,OAAO,CAAC,KAAK,IAAI,YAAY,EAAE,QAAQ,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { useSuspenseQuery } from '@tanstack/react-query'\nimport { noop } from 'lodash'\nimport { useCallback } from 'react'\n\nconst cacheKeyGenerator = (key: string) => [`AsyncStorageResource:${key}`]\n\ntype SetValue<TCacheData> = (_itemValue?: TCacheData | null) => Promise<void>\n\nexport function useAsyncStorage<TCacheData>(\n key: string,\n initialValue: TCacheData,\n throwOnError: boolean = false\n): [TCacheData, SetValue<TCacheData>] {\n const cacheKey = cacheKeyGenerator(key)\n\n const { data: value, refetch } = useSuspenseQuery<TCacheData>({\n queryKey: cacheKey,\n queryFn: () =>\n AsyncStorage.getItem(key)\n .then(storedValue => {\n if (!storedValue) return initialValue\n\n try {\n return JSON.parse(storedValue)\n } catch {\n return storedValue\n }\n })\n .catch(e => {\n if (!throwOnError) return initialValue\n\n return Promise.reject(e)\n }),\n })\n\n const setValue: SetValue<TCacheData> = useCallback(\n itemValue => {\n if (itemValue === null || itemValue === undefined) {\n return AsyncStorage.removeItem(key)\n .then(() => {\n refetch()\n })\n .catch(noop)\n }\n\n return AsyncStorage.setItem(key, JSON.stringify(itemValue))\n .then(() => {\n refetch()\n })\n .catch(e => {\n if (!throwOnError) return\n\n return Promise.reject(e)\n })\n },\n [throwOnError, key, refetch]\n )\n\n return [value || initialValue, setValue]\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.20.1-rc.0",
3
+ "version": "3.20.1-rc.1",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,5 +58,5 @@
58
58
  "react-native-url-polyfill": "^2.0.0",
59
59
  "typescript": "<5.6.0"
60
60
  },
61
- "gitHead": "f896400d25cd9b75b7fe6ed67fbda8b03eaade70"
61
+ "gitHead": "e0a908049cd839fc176a02a3addbf94ec4b9a42d"
62
62
  }
@@ -0,0 +1,313 @@
1
+ import { renderHook, act } from '@testing-library/react-hooks'
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3
+ import React, { Suspense } from 'react'
4
+ import AsyncStorage from '@react-native-async-storage/async-storage'
5
+ import { useAsyncStorage } from '../../hooks/use_async_storage'
6
+
7
+ jest.mock('@react-native-async-storage/async-storage')
8
+ jest.useFakeTimers()
9
+
10
+ afterAll(() => {
11
+ jest.useRealTimers()
12
+ })
13
+
14
+ const mockAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>
15
+
16
+ const createWrapper = () => {
17
+ const queryClient = new QueryClient({
18
+ defaultOptions: {
19
+ queries: {
20
+ retry: false,
21
+ },
22
+ },
23
+ })
24
+
25
+ return ({ children }: { children: React.ReactNode }) => (
26
+ <QueryClientProvider client={queryClient}>
27
+ <Suspense fallback={null}>{children}</Suspense>
28
+ </QueryClientProvider>
29
+ )
30
+ }
31
+
32
+ // Helper to wait for Suspense and async operations to resolve
33
+ const waitForQuery = async () => {
34
+ await act(async () => {
35
+ await Promise.resolve()
36
+ await Promise.resolve()
37
+ await Promise.resolve()
38
+ })
39
+ }
40
+
41
+ describe('useAsyncStorage', () => {
42
+ beforeEach(() => {
43
+ jest.clearAllMocks()
44
+ mockAsyncStorage.getItem.mockResolvedValue(null)
45
+ mockAsyncStorage.setItem.mockResolvedValue(undefined)
46
+ mockAsyncStorage.removeItem.mockResolvedValue(undefined)
47
+ })
48
+
49
+ afterEach(() => {
50
+ jest.restoreAllMocks()
51
+ })
52
+
53
+ describe('initialization', () => {
54
+ it('returns initial value when storage is empty', async () => {
55
+ mockAsyncStorage.getItem.mockResolvedValue(null)
56
+
57
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial-value'), {
58
+ wrapper: createWrapper(),
59
+ })
60
+
61
+ await waitForQuery()
62
+
63
+ expect(result.current).toBeDefined()
64
+ expect(result.current[0]).toBe('initial-value')
65
+ })
66
+
67
+ it('returns parsed JSON value from storage', async () => {
68
+ const storedValue = { foo: 'bar', count: 42 }
69
+ mockAsyncStorage.getItem.mockResolvedValue(JSON.stringify(storedValue))
70
+
71
+ const { result } = renderHook(() => useAsyncStorage('test-key', { default: 'value' }), {
72
+ wrapper: createWrapper(),
73
+ })
74
+
75
+ await waitForQuery()
76
+
77
+ expect(result.current).toBeDefined()
78
+ expect(result.current[0]).toEqual(storedValue)
79
+ })
80
+
81
+ it('returns raw string value when JSON parsing fails', async () => {
82
+ mockAsyncStorage.getItem.mockResolvedValue('not-valid-json{')
83
+
84
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial-value'), {
85
+ wrapper: createWrapper(),
86
+ })
87
+
88
+ await waitForQuery()
89
+
90
+ expect(result.current).toBeDefined()
91
+ expect(result.current[0]).toBe('not-valid-json{')
92
+ })
93
+
94
+ it('returns initial value when getItem throws an error', async () => {
95
+ const error = new Error('Storage error')
96
+ mockAsyncStorage.getItem.mockRejectedValue(error)
97
+
98
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial-value'), {
99
+ wrapper: createWrapper(),
100
+ })
101
+
102
+ await waitForQuery()
103
+
104
+ expect(result.current).toBeDefined()
105
+ expect(result.current[0]).toBe('initial-value')
106
+ })
107
+
108
+ it('returns initial value when getItem throws quota exceeded error', async () => {
109
+ const quotaError = new Error('database or disk is full (code 13): SQLITE_FULL')
110
+ mockAsyncStorage.getItem.mockRejectedValue(quotaError)
111
+
112
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial-value'), {
113
+ wrapper: createWrapper(),
114
+ })
115
+
116
+ await act(async () => {
117
+ await waitForQuery()
118
+ })
119
+
120
+ expect(result.current).toBeDefined()
121
+ expect(result.current[0]).toBe('initial-value')
122
+ })
123
+ })
124
+
125
+ describe('setValue', () => {
126
+ it('saves value to storage and refetches', async () => {
127
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
128
+ wrapper: createWrapper(),
129
+ })
130
+
131
+ await waitForQuery()
132
+
133
+ expect(result.current).toBeDefined()
134
+ expect(result.current[0]).toBe('initial')
135
+
136
+ const newValue = 'new-string-value'
137
+ await act(async () => {
138
+ await result.current[1](newValue)
139
+ })
140
+
141
+ expect(mockAsyncStorage.setItem).toHaveBeenCalledWith('test-key', JSON.stringify(newValue))
142
+ })
143
+
144
+ it('saves object value to storage', async () => {
145
+ const { result } = renderHook(() => useAsyncStorage('test-key', { initial: 'value' }), {
146
+ wrapper: createWrapper(),
147
+ })
148
+
149
+ await waitForQuery()
150
+
151
+ expect(result.current).toBeDefined()
152
+
153
+ const newValue = { initial: 'new-value', data: 'test' }
154
+ await act(async () => {
155
+ await result.current[1](newValue)
156
+ })
157
+
158
+ expect(mockAsyncStorage.setItem).toHaveBeenCalledWith('test-key', JSON.stringify(newValue))
159
+ })
160
+
161
+ it('removes item when value is null', async () => {
162
+ mockAsyncStorage.getItem.mockResolvedValue(JSON.stringify({ data: 'existing' }))
163
+
164
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
165
+ wrapper: createWrapper(),
166
+ })
167
+
168
+ await waitForQuery()
169
+
170
+ expect(result.current).toBeDefined()
171
+ expect(result.current[0]).toEqual({ data: 'existing' })
172
+
173
+ await act(async () => {
174
+ await result.current[1](null)
175
+ })
176
+
177
+ expect(mockAsyncStorage.removeItem).toHaveBeenCalledWith('test-key')
178
+ expect(mockAsyncStorage.setItem).not.toHaveBeenCalled()
179
+ })
180
+
181
+ it('removes item when value is undefined', async () => {
182
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
183
+ wrapper: createWrapper(),
184
+ })
185
+
186
+ await waitForQuery()
187
+
188
+ expect(result.current).toBeDefined()
189
+ expect(result.current[0]).toBe('initial')
190
+
191
+ await act(async () => {
192
+ await result.current[1](undefined)
193
+ })
194
+
195
+ expect(mockAsyncStorage.removeItem).toHaveBeenCalledWith('test-key')
196
+ expect(mockAsyncStorage.setItem).not.toHaveBeenCalled()
197
+ })
198
+
199
+ it('does not throw error when setItem throws quota exceeded error by default', async () => {
200
+ const quotaError = new Error('database or disk is full (code 13): SQLITE_FULL')
201
+ mockAsyncStorage.setItem.mockRejectedValue(quotaError)
202
+
203
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
204
+ wrapper: createWrapper(),
205
+ })
206
+
207
+ await waitForQuery()
208
+
209
+ expect(result.current).toBeDefined()
210
+ expect(result.current[0]).toBe('initial')
211
+
212
+ // Should not throw - errors are caught by default
213
+ let promiseResult: void | undefined
214
+ await act(async () => {
215
+ promiseResult = await result.current[1]('new-value')
216
+ })
217
+ expect(promiseResult).toBeUndefined()
218
+ })
219
+
220
+ it('propagates error when setItem throws quota exceeded error with throwOnError=true', async () => {
221
+ const quotaError = new Error('database or disk is full (code 13): SQLITE_FULL')
222
+ mockAsyncStorage.setItem.mockRejectedValue(quotaError)
223
+
224
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial', true), {
225
+ wrapper: createWrapper(),
226
+ })
227
+
228
+ await waitForQuery()
229
+
230
+ expect(result.current).toBeDefined()
231
+ expect(result.current[0]).toBe('initial')
232
+
233
+ await act(async () => {
234
+ await expect(result.current[1]('new-value')).rejects.toThrow('database or disk is full')
235
+ })
236
+ })
237
+
238
+ it('does not throw error when setItem throws other errors by default', async () => {
239
+ const error = new Error('Storage write failed')
240
+ mockAsyncStorage.setItem.mockRejectedValue(error)
241
+
242
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
243
+ wrapper: createWrapper(),
244
+ })
245
+
246
+ await waitForQuery()
247
+
248
+ expect(result.current).toBeDefined()
249
+ expect(result.current[0]).toBe('initial')
250
+
251
+ // Should not throw - errors are caught by default
252
+ let promiseResult: void | undefined
253
+ await act(async () => {
254
+ promiseResult = await result.current[1]('new-value')
255
+ })
256
+ expect(promiseResult).toBeUndefined()
257
+ })
258
+
259
+ it('does not throw error when removeItem throws an error by default', async () => {
260
+ const error = new Error('Remove failed')
261
+ mockAsyncStorage.removeItem.mockRejectedValue(error)
262
+
263
+ const { result } = renderHook(() => useAsyncStorage('test-key', 'initial'), {
264
+ wrapper: createWrapper(),
265
+ })
266
+
267
+ await waitForQuery()
268
+
269
+ expect(result.current).toBeDefined()
270
+ expect(result.current[0]).toBe('initial')
271
+
272
+ // Should not throw - errors are caught by default (via noop)
273
+ let promiseResult: void | undefined
274
+ await act(async () => {
275
+ promiseResult = await result.current[1](null)
276
+ })
277
+ expect(promiseResult).toBeUndefined()
278
+ })
279
+ })
280
+
281
+ describe('edge cases', () => {
282
+ it('handles complex nested objects', async () => {
283
+ const complexValue = {
284
+ nested: {
285
+ array: [1, 2, { deep: 'value' }],
286
+ date: '2024-01-01',
287
+ },
288
+ }
289
+ mockAsyncStorage.getItem.mockResolvedValue(JSON.stringify(complexValue))
290
+
291
+ const { result } = renderHook(() => useAsyncStorage('test-key', {}), {
292
+ wrapper: createWrapper(),
293
+ })
294
+
295
+ await waitForQuery()
296
+
297
+ expect(result.current).toBeDefined()
298
+ expect(result.current[0]).toEqual(complexValue)
299
+ })
300
+
301
+ it('uses correct cache key format', async () => {
302
+ const { result } = renderHook(() => useAsyncStorage('my-unique-key', 'value'), {
303
+ wrapper: createWrapper(),
304
+ })
305
+
306
+ await waitForQuery()
307
+
308
+ expect(result.current).toBeDefined()
309
+ expect(result.current[0]).toBe('value')
310
+ expect(mockAsyncStorage.getItem).toHaveBeenCalledWith('my-unique-key')
311
+ })
312
+ })
313
+ })
@@ -1,5 +1,6 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage'
2
2
  import { useSuspenseQuery } from '@tanstack/react-query'
3
+ import { noop } from 'lodash'
3
4
  import { useCallback } from 'react'
4
5
 
5
6
  const cacheKeyGenerator = (key: string) => [`AsyncStorageResource:${key}`]
@@ -8,36 +9,52 @@ type SetValue<TCacheData> = (_itemValue?: TCacheData | null) => Promise<void>
8
9
 
9
10
  export function useAsyncStorage<TCacheData>(
10
11
  key: string,
11
- initialValue: TCacheData
12
+ initialValue: TCacheData,
13
+ throwOnError: boolean = false
12
14
  ): [TCacheData, SetValue<TCacheData>] {
13
15
  const cacheKey = cacheKeyGenerator(key)
16
+
14
17
  const { data: value, refetch } = useSuspenseQuery<TCacheData>({
15
18
  queryKey: cacheKey,
16
19
  queryFn: () =>
17
- AsyncStorage.getItem(key).then(storedValue => {
18
- if (!storedValue) return initialValue
19
-
20
- try {
21
- return JSON.parse(storedValue)
22
- } catch {
23
- return storedValue
24
- }
25
- }),
20
+ AsyncStorage.getItem(key)
21
+ .then(storedValue => {
22
+ if (!storedValue) return initialValue
23
+
24
+ try {
25
+ return JSON.parse(storedValue)
26
+ } catch {
27
+ return storedValue
28
+ }
29
+ })
30
+ .catch(e => {
31
+ if (!throwOnError) return initialValue
32
+
33
+ return Promise.reject(e)
34
+ }),
26
35
  })
27
36
 
28
37
  const setValue: SetValue<TCacheData> = useCallback(
29
38
  itemValue => {
30
39
  if (itemValue === null || itemValue === undefined) {
31
- return AsyncStorage.removeItem(key).then(() => {
40
+ return AsyncStorage.removeItem(key)
41
+ .then(() => {
42
+ refetch()
43
+ })
44
+ .catch(noop)
45
+ }
46
+
47
+ return AsyncStorage.setItem(key, JSON.stringify(itemValue))
48
+ .then(() => {
32
49
  refetch()
33
50
  })
34
- }
51
+ .catch(e => {
52
+ if (!throwOnError) return
35
53
 
36
- return AsyncStorage.setItem(key, JSON.stringify(itemValue)).then(() => {
37
- refetch()
38
- })
54
+ return Promise.reject(e)
55
+ })
39
56
  },
40
- [key, refetch]
57
+ [throwOnError, key, refetch]
41
58
  )
42
59
 
43
60
  return [value || initialValue, setValue]