diff --git a/src/use-local-storage/index.ts b/src/use-local-storage/index.ts index a395aff5..78bd1568 100644 --- a/src/use-local-storage/index.ts +++ b/src/use-local-storage/index.ts @@ -9,7 +9,10 @@ function dispatchStorageEvent(key: string, newValue: string | null) { } } -const setLocalStorageItem = (key: string, value: any, serializer: (value: T) => string) => { +export type Serializer = (value: T) => string; +export type Deserializer = (value: string) => T; + +const setLocalStorageItem = (key: string, value: any, serializer: Serializer) => { if (typeof window !== 'undefined') { const stringifiedValue = serializer(value); try { @@ -35,14 +38,11 @@ const removeLocalStorageItem = (key: string) => { } }; -const getLocalStorageItem = (key: string, deserializer: (value: string) => T) => { +const getLocalStorageItem = (key: string, deserializer: Deserializer) => { if (typeof window !== 'undefined') { try { const value = window.localStorage.getItem(key); - if (value) { - return deserializer(value); - } - return value; + return value === null ? null : deserializer(value); } catch (e) { console.warn(e); return null; @@ -67,41 +67,26 @@ const getServerSnapshotWithoutServerValue = () => { // eslint-disable-next-line @typescript-eslint/ban-types -- workaround TypeScript bug const isFunction = (x: unknown): x is Function => typeof x === 'function'; -const getLocalStorageParser = (options?: UseLocalStorageRawOption | UseLocalStorageParserOption): UseLocalStorageParserOption => { - if (typeof options === 'undefined') { - return { - serializer: JSON.stringify, - deserializer: JSON.parse - }; - } - if ('raw' in options) { - return { - serializer: String, - deserializer: (value: string) => value as T - }; - } - if ('serializer' in options && 'deserializer' in options) { - return options; - } - return { - serializer: JSON.stringify, - deserializer: JSON.parse - }; -}; +const identity = (x: any) => x; -interface UseLocalStorageRawOption { +export interface UseLocalStorageRawOption { raw: true } -interface UseLocalStorageParserOption { - serializer: (value: T) => string, - deserializer: (value: string) => T +export interface UseLocalStorageParserOption { + raw?: false, + serializer: Serializer, + deserializer: Deserializer } +type NotUndefined = T extends undefined ? never : T; + /** @see https://foxact.skk.moe/use-local-storage */ -export function useLocalStorage(key: string, serverValue?: T, options?: UseLocalStorageRawOption | UseLocalStorageParserOption) { - const { serializer, deserializer } = getLocalStorageParser(options); - const getSnapshot = () => getLocalStorageItem(key, deserializer) as T | null; +export function useLocalStorage(key: string, serverValue?: NotUndefined | undefined, options?: UseLocalStorageRawOption | UseLocalStorageParserOption>) { + const serializer: Serializer = options?.raw ? identity : (options?.serializer ?? JSON.stringify); + const deserializer: Deserializer = options?.raw ? identity : (options?.deserializer ?? JSON.parse); + + const getClientSnapshot = () => getLocalStorageItem(key, deserializer); // If the serverValue is provided, we pass it to useSES' getServerSnapshot, which will be used during SSR // If the serverValue is not provided, we don't pass it to useSES, which will cause useSES to opt-in client-side rendering @@ -111,7 +96,7 @@ export function useLocalStorage(key: string, serverVa const store = useSyncExternalStore( subscribeToLocalStorage, - getSnapshot, + getClientSnapshot, getServerSnapshot ); @@ -122,7 +107,7 @@ export function useLocalStorage(key: string, serverVa ? v(store ?? null) : v; - if (nextState == null) { + if (nextState === null) { removeLocalStorageItem(key); } else { setLocalStorageItem(key, nextState, serializer);