Skip to content

Instantly share code, notes, and snippets.

@danielberndt
Last active August 19, 2021 10:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielberndt/a7b72419c345ba4110d9f8b6ae385493 to your computer and use it in GitHub Desktop.
Save danielberndt/a7b72419c345ba4110d9f8b6ae385493 to your computer and use it in GitHub Desktop.
useLocalStorageState
import {useEffect, useMemo, useRef, useState} from "react";
const getStorageMap = (storageGetter) => {
let storage;
try {
storage = storageGetter();
} catch {
return {
storageGet() {
return null;
},
storageSet() {
return false;
},
storageRemove() {
return false;
},
};
}
return {
storageGet(key) {
try {
const content = storage.getItem(key);
return content ? JSON.parse(content) : null;
} catch (e) {
return null;
}
},
storageSet(key, val) {
try {
storage.setItem(key, JSON.stringify(val));
return true;
} catch (e) {
return false;
}
},
storageRemove(key) {
try {
storage.removeItem(key);
return true;
} catch (e) {
return false;
}
},
};
};
/**
* `lsStorage` contains a wrapper for the provided storage (either `localStorage` or `sessionStorage`)
* that automatically transform data from and to json.
* It also deals nicely with browsers that forbid accessing `localStorage`
*/
const storageWrapper = getStorageMap(() => window.localStorage);
const getStorageValOrDefaultWithKey = (key, defaultVal) => {
if (!key) return {key, value: null};
const storageVal = storageWrapper.storageGet(key);
const value = storageVal === null ? defaultVal : storageVal;
return {key, value};
};
const listeners = new Set();
const addListener = (cb) => {
listeners.add(cb);
return () => listeners.delete(cb);
};
const notifyListeners = (...args) => {
listeners.forEach((l) => l(...args));
};
/**
* sample usage:
* const [val, setVal] = useLocalStorage('my-storage-key')
*
* full example:
* const [val, setVal, {clear}] = useLocalStorage('my-storage-key', [defaultValueIfStorageHasNoValueYet])
*
*/
export const useLocalStorageState = (key, defaultVal) => {
// `data` contains both the current key and the value. The `key` is stored to allow
// reacting immediately in case the key passed in above has changed. (rather than having to wait for an e.g. `useEffect`)
const [data, setData] = useState(() => getStorageValOrDefaultWithKey(key, defaultVal));
// `meRef` contains a unique identifier. This way the event listener knows which hook has been calling
// and doesn't need to be updated again
const meRef = useRef();
if (!meRef.current) meRef.current = {};
// nextVal is a fallback only used when the key changes so we can immediately pass the `key`'s real value
let nextVal = null;
// if the passed key differs from the last seen key, update the data immediately
if (key !== data.key) {
nextVal = getStorageValOrDefaultWithKey(key, defaultVal);
setData(nextVal);
}
// start listening for changes in case another hook changes the same key
useEffect(
() =>
addListener((eventKey, val, by) => {
if (key !== eventKey) return;
if (by === meRef.current) return;
setData(val);
}),
[key]
);
// create a reference of the passed `defaultVal`. This allows the `useMemo` below to access the defaultVal
// without having to add it to the dependency array. (Otherwise calling e.g. `useLocalStorageState('key', [])`)
// would result in not really memoizing the handlers below as the `[]` is always a new array different from the
// one passed in before.
const defaultValueRef = useRef(defaultVal);
useEffect(() => {
defaultValueRef.current = defaultVal;
}, [defaultVal]);
// memoize `setVal` and `clear` as setters are expected to not change in hooks.
const handlers = useMemo(
() => ({
// setVal supports both `setVal(nextValue)` and `setVal(prevVal => prevVal + 1)` shapes
setVal: (next) => {
if (typeof next === "function") {
setData((prev) => {
const val = next(prev.value);
storageWrapper.storageSet(key, val);
notifyListeners(key, val, meRef.current);
return {key, value: val};
});
} else {
storageWrapper.storageSet(key, next);
setData({key, value: next});
notifyListeners(key, next, meRef.current);
}
},
clear: () => {
storageWrapper.storageRemove(key);
setData({key, value: defaultValueRef.current});
notifyListeners(key, defaultValueRef.current, meRef.current);
},
}),
[key]
);
return [nextVal ? nextVal.value : data.value, handlers.setVal, {clear: handlers.clear}];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment