Last active
August 19, 2021 10:57
-
-
Save danielberndt/a7b72419c345ba4110d9f8b6ae385493 to your computer and use it in GitHub Desktop.
useLocalStorageState
This file contains 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 {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