jotai: createJSONStorage does not properly subscribe to updates caused by RESET
Summary
When an item is removed from storage, a storage
event fires with null
as the incoming value. The storage event handler in createJSONStorage
ignores null
values from incoming storage
events.
This is caused by a check for a non-falsy e.newValue
here:
https://github.com/pmndrs/jotai/blob/main/src/vanilla/utils/atomWithStorage.ts#L83
When setAtom(RESET)
is called on an atomWithStorage
, subscribers in other windows do not get the reset update.
Link to reproduction
I’ve created a minimal reproduction of the issue on codesandbox here:
https://codesandbox.io/s/jotai-storage-bug-reset-u9doo3?file=/src/App.tsx
Like with #1812 you might have to open the preview iframe itself (https://u9doo3.csb.app/) in separate windows to avoid permissions errors.
Fix discussion
I think that in order to fix this, the interface of the (Sync/Async)Storage.subscribe
argument callback
needs to be updated to accept NO_STORAGE_VALUE
, so that the Storage can inform atomWithStorage
that a reset occurred in another window.
--- a/src/vanilla/utils/atomWithStorage.ts
+++ b/src/vanilla/utils/atomWithStorage.ts
@@ -15,14 +15,14 @@ export interface AsyncStorage<Value> {
getItem: (key: string) => Promise<Value | typeof NO_STORAGE_VALUE>
setItem: (key: string, newValue: Value) => Promise<void>
removeItem: (key: string) => Promise<void>
- subscribe?: (key: string, callback: (value: Value) => void) => Unsubscribe
+ subscribe?: (key: string, callback: (value: Value | typeof NO_STORAGE_VALUE) => void) => Unsubscribe
}
export interface SyncStorage<Value> {
getItem: (key: string) => Value | typeof NO_STORAGE_VALUE
setItem: (key: string, newValue: Value) => void
removeItem: (key: string) => void
- subscribe?: (key: string, callback: (value: Value) => void) => Unsubscribe
+ subscribe?: (key: string, callback: (value: Value | typeof NO_STORAGE_VALUE) => void) => Unsubscribe
}
export interface AsyncStringStorage {
@@ -83,10 +83,13 @@ export function createJSONStorage<Value>(
const storageEventCallback = (e: StorageEvent) => {
if (
e.storageArea === getStringStorage() &&
- e.key === key &&
- e.newValue
+ e.key === key
) {
- callback(JSON.parse(e.newValue))
+ try {
+ callback(JSON.parse(e.newValue ?? ''))
+ } catch (e) {
+ callback(NO_STORAGE_VALUE)
+ }
}
}
window.addEventListener('storage', storageEventCallback)
@@ -126,7 +129,9 @@ export function atomWithStorage<Value>(
}
let unsub: Unsubscribe | undefined
if (storage.subscribe) {
- unsub = storage.subscribe(key, setAtom)
+ unsub = storage.subscribe(key, (nextValue) => {
+ setAtom(nextValue === NO_STORAGE_VALUE ? initialValue : nextValue)
+ })
}
return unsub
}
Currently, the only place the NO_STORAGE_VALUE
symbol is exposed to consumers is with an unstable_
prefix.
That change would technically leak NO_STORAGE_VALUE
as part of the exposed types for Sync/AsyncStorage
.
It might make sense to remove the unstable prefix from the exported NO_STORAGE_VALUE
symbol because of that?
Semi-related: Another reason it might make sense to expose NO_STORAGE_VALUE
is because I don’t think you can create a custom storage (one that doesn’t use createJSONStorage
, such as would be required to make a validated storage - #1198) without using that symbol either.
Check List
Please do not ask questions in issues.
- I’ve already opened a discussion before opening this issue, or already discussed in other media.
Please include a minimal reproduction.
- I’ve added a link to a typescript playground or codesandbox with a minimal reproduction.
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 15 (15 by maintainers)
@nderscore Would you like to draft a PR?
oops, I missed that part.
Yeah, this would only work: