Skip to content

Commit c9c66ca

Browse files
committed
fix(utils): avoid catching storage error in atomWithStorage
1 parent 704ff3c commit c9c66ca

File tree

3 files changed

+53
-39
lines changed

3 files changed

+53
-39
lines changed

src/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { splitAtom } from './utils/splitAtom'
1313
export { atomWithDefault } from './utils/atomWithDefault'
1414
export { waitForAll } from './utils/waitForAll'
1515
export {
16+
NO_STORAGE_VALUE,
1617
atomWithStorage,
1718
atomWithHash,
1819
createJSONStorage,

src/utils/atomWithStorage.ts

+46-32
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { atom } from 'jotai'
22
import type { WritableAtom } from 'jotai'
33
import { RESET } from './constants'
44

5+
export const NO_STORAGE_VALUE = Symbol()
6+
57
type Unsubscribe = () => void
68

79
type SetStateActionWithReset<Value> =
@@ -10,15 +12,15 @@ type SetStateActionWithReset<Value> =
1012
| ((prev: Value) => Value | typeof RESET)
1113

1214
export interface AsyncStorage<Value> {
13-
getItem: (key: string) => Promise<Value>
15+
getItem: (key: string) => Promise<Value | typeof NO_STORAGE_VALUE>
1416
setItem: (key: string, newValue: Value) => Promise<void>
1517
removeItem: (key: string) => Promise<void>
1618
delayInit?: boolean
1719
subscribe?: (key: string, callback: (value: Value) => void) => Unsubscribe
1820
}
1921

2022
export interface SyncStorage<Value> {
21-
getItem: (key: string) => Value
23+
getItem: (key: string) => Value | typeof NO_STORAGE_VALUE
2224
setItem: (key: string, newValue: Value) => void
2325
removeItem: (key: string) => void
2426
delayInit?: boolean
@@ -46,45 +48,56 @@ export function createJSONStorage<Value>(
4648
): SyncStorage<Value>
4749

4850
export function createJSONStorage<Value>(
49-
getStringStorage: () => AsyncStringStorage | SyncStringStorage
51+
getStringStorage: () => AsyncStringStorage | SyncStringStorage | undefined
5052
): AsyncStorage<Value> | SyncStorage<Value> {
5153
let lastStr: string | undefined
5254
let lastValue: any
53-
return {
55+
const storage: AsyncStorage<Value> | SyncStorage<Value> = {
5456
getItem: (key) => {
5557
const parse = (str: string | null) => {
5658
str = str || ''
5759
if (lastStr !== str) {
58-
lastValue = JSON.parse(str)
60+
try {
61+
lastValue = JSON.parse(str)
62+
} catch {
63+
return NO_STORAGE_VALUE
64+
}
5965
lastStr = str
6066
}
6167
return lastValue
6268
}
63-
const str = getStringStorage().getItem(key)
69+
const str = getStringStorage()?.getItem(key) ?? null
6470
if (str instanceof Promise) {
6571
return str.then(parse)
6672
}
6773
return parse(str)
6874
},
6975
setItem: (key, newValue) =>
70-
getStringStorage().setItem(key, JSON.stringify(newValue)),
71-
removeItem: (key) => getStringStorage().removeItem(key),
76+
getStringStorage()?.setItem(key, JSON.stringify(newValue)),
77+
removeItem: (key) => getStringStorage()?.removeItem(key),
7278
}
73-
}
74-
75-
const defaultStorage = createJSONStorage(() => localStorage)
76-
defaultStorage.subscribe = (key, callback) => {
77-
const storageEventCallback = (e: StorageEvent) => {
78-
if (e.key === key && e.newValue) {
79-
callback(JSON.parse(e.newValue))
79+
if (typeof window !== 'undefined') {
80+
storage.subscribe = (key, callback) => {
81+
const storageEventCallback = (e: StorageEvent) => {
82+
if (e.key === key && e.newValue) {
83+
callback(JSON.parse(e.newValue))
84+
}
85+
}
86+
window.addEventListener('storage', storageEventCallback)
87+
return () => {
88+
window.removeEventListener('storage', storageEventCallback)
89+
}
8090
}
8191
}
82-
window.addEventListener('storage', storageEventCallback)
83-
return () => {
84-
window.removeEventListener('storage', storageEventCallback)
85-
}
92+
return storage
8693
}
8794

95+
const defaultStorage = createJSONStorage(() =>
96+
typeof window !== 'undefined'
97+
? window.localStorage
98+
: (undefined as unknown as Storage)
99+
)
100+
88101
export function atomWithStorage<Value>(
89102
key: string,
90103
initialValue: Value,
@@ -116,15 +129,11 @@ export function atomWithStorage<Value>(
116129
| AsyncStorage<Value> = defaultStorage as SyncStorage<Value>
117130
) {
118131
const getInitialValue = () => {
119-
try {
120-
const value = storage.getItem(key)
121-
if (value instanceof Promise) {
122-
return value.catch(() => initialValue)
123-
}
124-
return value
125-
} catch {
126-
return initialValue
132+
const value = storage.getItem(key)
133+
if (value instanceof Promise) {
134+
return value.then((v) => (v === NO_STORAGE_VALUE ? initialValue : v))
127135
}
136+
return value === NO_STORAGE_VALUE ? initialValue : value
128137
}
129138

130139
const baseAtom = atom(storage.delayInit ? initialValue : getInitialValue())
@@ -175,14 +184,22 @@ export function atomWithHash<Value>(
175184
initialValue: Value,
176185
options?: {
177186
serialize?: (val: Value) => string
178-
deserialize?: (str: string) => Value
187+
deserialize?: (str: string | null) => Value | typeof NO_STORAGE_VALUE
179188
delayInit?: boolean
180189
replaceState?: boolean
181190
subscribe?: (callback: () => void) => () => void
182191
}
183192
): WritableAtom<Value, SetStateActionWithReset<Value>> {
184193
const serialize = options?.serialize || JSON.stringify
185-
const deserialize = options?.deserialize || JSON.parse
194+
const deserialize =
195+
options?.deserialize ||
196+
((str) => {
197+
try {
198+
return JSON.parse(str || '')
199+
} catch {
200+
return NO_STORAGE_VALUE
201+
}
202+
})
186203
const subscribe =
187204
options?.subscribe ||
188205
((callback) => {
@@ -195,9 +212,6 @@ export function atomWithHash<Value>(
195212
getItem: (key) => {
196213
const searchParams = new URLSearchParams(location.hash.slice(1))
197214
const storedValue = searchParams.get(key)
198-
if (storedValue === null) {
199-
throw new Error('no value stored')
200-
}
201215
return deserialize(storedValue)
202216
},
203217
setItem: (key, newValue) => {

tests/utils/atomWithStorage.test.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Suspense } from 'react'
22
import { fireEvent, render, waitFor } from '@testing-library/react'
33
import { useAtom } from 'jotai'
44
import {
5+
NO_STORAGE_VALUE,
56
RESET,
67
atomWithHash,
78
atomWithStorage,
@@ -18,7 +19,7 @@ describe('atomWithStorage (sync)', () => {
1819
const dummyStorage = {
1920
getItem: (key: string) => {
2021
if (!(key in storageData)) {
21-
throw new Error('no value stored')
22+
return NO_STORAGE_VALUE
2223
}
2324
return storageData[key] as number
2425
},
@@ -195,7 +196,7 @@ describe('atomWithStorage (async)', () => {
195196
getItem: async (key: string) => {
196197
await new Promise((r) => setTimeout(r, 100))
197198
if (!(key in asyncStorageData)) {
198-
throw new Error('no value stored')
199+
return NO_STORAGE_VALUE
199200
}
200201
return asyncStorageData[key] as number
201202
},
@@ -310,14 +311,12 @@ describe('atomWithStorage (async)', () => {
310311
})
311312
})
312313

313-
describe('atomWithStorage (no storage) (#949)', () => {
314-
it('can throw in createJSONStorage', async () => {
314+
describe('atomWithStorage (without localStorage) (#949)', () => {
315+
it('createJSONStorage without localStorage', async () => {
315316
const countAtom = atomWithStorage(
316317
'count',
317318
1,
318-
createJSONStorage(() => {
319-
throw new Error('no storage')
320-
})
319+
createJSONStorage(() => undefined as any)
321320
)
322321

323322
const Counter = () => {

0 commit comments

Comments
 (0)