-
-
Notifications
You must be signed in to change notification settings - Fork 615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(utils): allow providing a subscribe method to createJSONStorage util #2539
Changes from 1 commit
b1f194e
0657feb
82af4a6
640f4ad
1044dfa
38bb47e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,8 +5,21 @@ import { RESET } from './constants.ts' | |||||
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> => | ||||||
typeof (x as any)?.then === 'function' | ||||||
|
||||||
type Subscribe<Value> = ( | ||||||
key: string, | ||||||
callback: (value: Value) => void, | ||||||
initialValue: Value, | ||||||
) => Unsubscribe | ||||||
|
||||||
type Unsubscribe = () => void | ||||||
|
||||||
type SubscribeHandler<Value> = ( | ||||||
subscribe: Subscribe<Value>, | ||||||
key: string, | ||||||
callback: (value: Value) => void, | ||||||
initialValue: Value, | ||||||
) => Unsubscribe | ||||||
|
||||||
type SetStateActionWithReset<Value> = | ||||||
| Value | ||||||
| typeof RESET | ||||||
|
@@ -38,12 +51,14 @@ export interface AsyncStringStorage { | |||||
getItem: (key: string) => PromiseLike<string | null> | ||||||
setItem: (key: string, newValue: string) => PromiseLike<void> | ||||||
removeItem: (key: string) => PromiseLike<void> | ||||||
subscribe?: Subscribe<string | null> | ||||||
} | ||||||
|
||||||
export interface SyncStringStorage { | ||||||
getItem: (key: string) => string | null | ||||||
setItem: (key: string, newValue: string) => void | ||||||
removeItem: (key: string) => void | ||||||
subscribe?: Subscribe<string | null> | ||||||
} | ||||||
|
||||||
export function withStorageValidator<Value>( | ||||||
|
@@ -114,6 +129,42 @@ export function createJSONStorage<Value>( | |||||
): AsyncStorage<Value> | SyncStorage<Value> { | ||||||
let lastStr: string | undefined | ||||||
let lastValue: Value | ||||||
|
||||||
const webStorageSubscribe: Subscribe<Value> = (key, callback) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still string based, right?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. before this PR, subscribe method was defined like this: subscribe?: (
key: string,
callback: (value: Value) => void,
initialValue: Value,
) => Unsubscribe and the callback would accept value of type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hey @dai-shi. I finally got some time to work on this. I would appreciate your opinion (and advice) on typing issues. Regarding this code suggestion, |
||||||
if (!(getStringStorage() instanceof window.Storage)) { | ||||||
return () => {} | ||||||
} | ||||||
const storageEventCallback = (e: StorageEvent) => { | ||||||
if (e.storageArea === getStringStorage() && e.key === key) { | ||||||
callback((e.newValue || '') as Value) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems wrong:
Suggested change
|
||||||
} | ||||||
} | ||||||
window.addEventListener('storage', storageEventCallback) | ||||||
return () => { | ||||||
window.removeEventListener('storage', storageEventCallback) | ||||||
} | ||||||
} | ||||||
|
||||||
const handleSubscribe: SubscribeHandler<Value> = ( | ||||||
subscriber, | ||||||
key, | ||||||
callback, | ||||||
initialValue, | ||||||
) => { | ||||||
function callbackWithParser(v: Value) { | ||||||
let newValue: Value | ||||||
try { | ||||||
newValue = JSON.parse((v as string) || '') | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you suggest to ensure this? should we make change the generic types to this: <Value extends string | null> or should we use something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I mean |
||||||
} catch { | ||||||
newValue = initialValue | ||||||
} | ||||||
|
||||||
callback(newValue as Value) | ||||||
} | ||||||
|
||||||
return subscriber(key, callbackWithParser, initialValue) | ||||||
} | ||||||
|
||||||
const storage: AsyncStorage<Value> | SyncStorage<Value> = { | ||||||
getItem: (key, initialValue) => { | ||||||
const parse = (str: string | null) => { | ||||||
|
@@ -141,30 +192,18 @@ export function createJSONStorage<Value>( | |||||
), | ||||||
removeItem: (key) => getStringStorage()?.removeItem(key), | ||||||
} | ||||||
|
||||||
if ( | ||||||
typeof window !== 'undefined' && | ||||||
typeof window.addEventListener === 'function' && | ||||||
window.Storage | ||||||
typeof window.addEventListener === 'function' | ||||||
) { | ||||||
storage.subscribe = (key, callback, initialValue) => { | ||||||
if (!(getStringStorage() instanceof window.Storage)) { | ||||||
return () => {} | ||||||
} | ||||||
const storageEventCallback = (e: StorageEvent) => { | ||||||
if (e.storageArea === getStringStorage() && e.key === key) { | ||||||
let newValue: Value | ||||||
try { | ||||||
newValue = JSON.parse(e.newValue || '') | ||||||
} catch { | ||||||
newValue = initialValue | ||||||
} | ||||||
callback(newValue) | ||||||
} | ||||||
} | ||||||
window.addEventListener('storage', storageEventCallback) | ||||||
return () => { | ||||||
window.removeEventListener('storage', storageEventCallback) | ||||||
} | ||||||
if (getStringStorage()?.subscribe) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIR, this can throw. So, we can't call it here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the and this code is the same as before: jotai/src/vanilla/utils/atomWithStorage.ts Line 166 in a4dc985
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, my bad. I was thinking of old implementation. We catch it already. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My first comment was actually right. We can't call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also used this in Next.js but forgot to upgrade and test the new version. this was on me, sorry. I opened a new PR (#2585) for a possible fix (and a test for detecting this problem) but I would appreciate your opinion. |
||||||
storage.subscribe = handleSubscribe.bind( | ||||||
null, | ||||||
getStringStorage()!.subscribe as unknown as Subscribe<Value>, | ||||||
) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. applied a few changes to fix this. I would appreciate a review of them. |
||||||
} else if (window.Storage) { | ||||||
storage.subscribe = handleSubscribe.bind(null, webStorageSubscribe) | ||||||
} | ||||||
} | ||||||
return storage | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, we don't need this type alias. (But, not 100% sure.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would something like this be more acceptable?:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I meant to inline it as it's used just once.