Skip to content

Instantly share code, notes, and snippets.

@ephys
Last active February 15, 2022 02:21
Show Gist options
  • Select an option

  • Save ephys/79974c286e92665dcaae9c8f5344afaf to your computer and use it in GitHub Desktop.

Select an option

Save ephys/79974c286e92665dcaae9c8f5344afaf to your computer and use it in GitHub Desktop.
React-hook like useState but synchronized with localStorage & sessionStorage (and the others)
/**
* 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