Last active
May 1, 2024 20:35
-
-
Save mhsattarian/7e9e93b29ec4d11ef31c40e85052e8be to your computer and use it in GitHub Desktop.
Jotai's createJSONStorage code with few tweaks to support custom subscribe functionality for persist storages
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AsyncStorage, SyncStorage } from 'jotai/vanilla/utils/atomWithStorage'; | |
type Unsubscribe = () => void; | |
type Subscribe<Value> = ( | |
key: string, | |
callback: (value: Value) => void, | |
initialValue: Value | |
) => Unsubscribe; | |
type SubscribeHandler<Value> = ( | |
subscribe: Subscribe<Value>, | |
key: string, | |
callback: (value: Value) => void, | |
initialValue: Value | |
) => Unsubscribe; | |
export interface AsyncStringStorage<V = string> { | |
getItem: (key: string) => PromiseLike<string | null>; | |
setItem: (key: string, newValue: string) => PromiseLike<void>; | |
removeItem: (key: string) => PromiseLike<void>; | |
subscribe?: Subscribe<V>; | |
} | |
export interface SyncStringStorage<V = string> { | |
getItem: (key: string) => string | null; | |
setItem: (key: string, newValue: string) => void; | |
removeItem: (key: string) => void; | |
subscribe?: Subscribe<V>; | |
} | |
type JsonStorageOptions<V> = { | |
reviver?: (key: string, value: unknown) => unknown; | |
replacer?: (key: string, value: unknown) => unknown; | |
subscribe?: Subscribe<V>; | |
}; | |
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> => | |
typeof (x as any)?.then === 'function'; | |
export function createJSONStorage__custom<Value>( | |
getStringStorage: () => AsyncStringStorage, | |
options?: JsonStorageOptions<Value> | |
): AsyncStorage<Value>; | |
export function createJSONStorage__custom<Value>( | |
getStringStorage: () => SyncStringStorage, | |
options?: JsonStorageOptions<Value> | |
): SyncStorage<Value>; | |
export function createJSONStorage__custom<Value>( | |
getStringStorage: () => | |
| AsyncStringStorage<Value> | |
| SyncStringStorage<Value> | |
| undefined = () => { | |
try { | |
return window.localStorage; | |
} catch (e) { | |
if (process.env.NODE_ENV !== 'production') { | |
if (typeof window !== 'undefined') { | |
console.warn(e); | |
} | |
} | |
return undefined; | |
} | |
}, | |
options?: JsonStorageOptions<Value> | |
): AsyncStorage<Value> | SyncStorage<Value> { | |
let lastStr: string | undefined; | |
let lastValue: Value; | |
const webStorageSubscribe = ((key, callback) => { | |
const storageEventCallback = (e: StorageEvent) => { | |
if (e.storageArea === getStringStorage() && e.key === key) { | |
callback((e.newValue || '') as Value); | |
} | |
}; | |
window.addEventListener('storage', storageEventCallback); | |
return () => { | |
window.removeEventListener('storage', storageEventCallback); | |
}; | |
}) satisfies Subscribe<Value>; | |
const handleSubscribe: SubscribeHandler<Value> = ( | |
subscriber, | |
key, | |
callback, | |
initialValue | |
) => { | |
function cb(v: Value) { | |
let newValue: Value; | |
try { | |
newValue = JSON.parse((v as string) || ''); | |
} catch { | |
newValue = initialValue; | |
} | |
callback(newValue); | |
} | |
return subscriber(key, cb, initialValue); | |
}; | |
const storage: AsyncStorage<Value> | SyncStorage<Value> = { | |
getItem: (key, initialValue) => { | |
const parse = (str: string | null) => { | |
str = str || ''; | |
if (lastStr !== str) { | |
try { | |
lastValue = JSON.parse(str, options?.reviver); | |
} catch { | |
return initialValue; | |
} | |
lastStr = str; | |
} | |
return lastValue; | |
}; | |
const str = getStringStorage()?.getItem(key) ?? null; | |
if (isPromiseLike(str)) { | |
return str.then(parse) as never; | |
} | |
return parse(str) as never; | |
}, | |
setItem: (key, newValue) => | |
getStringStorage()?.setItem( | |
key, | |
JSON.stringify(newValue, options?.replacer) | |
), | |
removeItem: (key) => getStringStorage()?.removeItem(key), | |
}; | |
if ( | |
typeof window !== 'undefined' && | |
typeof window.addEventListener === 'function' | |
) { | |
storage.subscribe = handleSubscribe.bind( | |
null, | |
getStringStorage()?.subscribe ?? webStorageSubscribe | |
); | |
} | |
return storage; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
here's the code to use the custom
createJSONStorage
function above to implement persist-with-cookies for Jotai atoms:Note that this code implements a poor way of handling browser feature detection and compatibility.