Last active
February 15, 2022 02:21
-
-
Save ephys/79974c286e92665dcaae9c8f5344afaf to your computer and use it in GitHub Desktop.
React-hook like useState but synchronized with localStorage & sessionStorage (and the others)
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
| /** | |
| * Source: https://gist.github.com/Ephys/79974c286e92665dcaae9c8f5344afaf | |
| */ | |
| import { useState, useEffect, useCallback, useRef } from 'react'; | |
| const eventTargets = new WeakMap(); | |
| function getEventTarget(storage: Storage) { | |
| if (eventTargets.has(storage)) { | |
| return eventTargets.get(storage); | |
| } | |
| let eventTarget; | |
| try { | |
| eventTarget = new EventTarget(); | |
| } catch (e) { | |
| // fallback to a div as EventTarget on Safari | |
| // because EventTarget is not constructable on Safari | |
| eventTarget = document.createElement('div'); | |
| } | |
| eventTargets.set(storage, eventTarget); | |
| return eventTarget; | |
| } | |
| export type TStorageSetValue<T> = (newValue: T | undefined | ((oldValue: T) => T)) => void; | |
| export type TJsonSerializable = number | boolean | string | null | |
| | Array<TJsonSerializable> | |
| | { [key: string]: TJsonSerializable }; | |
| let lastHookId = 0; | |
| export function createStorageHook(storage: Storage = new Storage()) { | |
| // window.onstorage only triggers cross-realm. This is used to notify other useLocalStorage on the same page that it changed | |
| return function useStorage<T extends TJsonSerializable>(key: string, defaultValue: T = null): [ | |
| /* get */ T, | |
| /* set */ TStorageSetValue<T>, | |
| ] { | |
| const hookIdRef = useRef(null); | |
| if (hookIdRef.current === null) { | |
| hookIdRef.current = lastHookId++; | |
| } | |
| const defaultValueRef = useRef(Object.freeze(defaultValue)); | |
| const eventTarget = getEventTarget(storage); | |
| const [value, setValueState] = useState(() => { | |
| const _value = localStorage.getItem(key); | |
| if (_value == null) { | |
| return defaultValueRef.current; | |
| } | |
| try { | |
| return JSON.parse(_value); | |
| } catch (e) { | |
| console.error('use-local-storage: invalid stored value format, resetting to default'); | |
| console.error(e); | |
| return defaultValueRef.current; | |
| } | |
| }); | |
| const currentValue = useRef(value); | |
| currentValue.current = value; | |
| const setValue: TStorageSetValue<T> = useCallback((val: T | ((oldVal: T) => T)) => { | |
| if (typeof val === 'function') { | |
| val = val(currentValue.current); | |
| } | |
| if (currentValue.current === val) { | |
| return; | |
| } | |
| // removeItem | |
| if (val === undefined) { | |
| currentValue.current = defaultValueRef.current; | |
| setValueState(defaultValueRef.current); | |
| if (localStorage.getItem(key) == null) { | |
| return; | |
| } | |
| localStorage.removeItem(key); | |
| } else { | |
| const stringified = JSON.stringify(val); | |
| currentValue.current = val; | |
| setValueState(val); | |
| if (stringified === localStorage.getItem(key)) { | |
| return; | |
| } | |
| localStorage.setItem(key, stringified); | |
| } | |
| eventTarget.dispatchEvent(new CustomEvent(`uls:storage:${key}`, { detail: { val, sourceHook: hookIdRef.current } })); | |
| }, [eventTarget, key]); | |
| useEffect(() => { | |
| function crossRealmOnChange(e: StorageEvent) { | |
| if (e.key !== key) { | |
| return; | |
| } | |
| try { | |
| setValue(JSON.parse(e.newValue)); | |
| } catch (_ignore) { | |
| /* ignore */ | |
| } | |
| } | |
| function sameRealmOnChange(e: CustomEvent) { | |
| // don't act on events we sent | |
| if (e.detail.sourceHook === hookIdRef.current) { | |
| return; | |
| } | |
| setValue(e.detail.val); // "val" is wrapped in an object to prevent undefined from being translated into null | |
| } | |
| eventTarget.addEventListener(`uls:storage:${key}`, sameRealmOnChange); | |
| window.addEventListener('storage', crossRealmOnChange); | |
| return () => { | |
| eventTarget.removeEventListener(`uls:storage:${key}`, sameRealmOnChange); | |
| window.removeEventListener('storage', crossRealmOnChange); | |
| }; | |
| }, [eventTarget, key, setValue]); | |
| return [value, setValue]; | |
| }; | |
| } | |
| // in non-browser contexts, we fallback to useState | |
| function useSsrStorageHook<T>(_key: string, defaultValue: T): [T, SetStateFunction<T>] { | |
| return useState(defaultValue); | |
| } | |
| export const useLocalStorage = typeof localStorage === 'undefined' ? useSsrStorageHook : createStorageHook(localStorage); | |
| export const useSessionStorage = typeof sessionStorage === 'undefined' ? useSsrStorageHook : createStorageHook(sessionStorage); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment