Created
October 11, 2019 09:59
-
-
Save freddi301/c71c53920fa45b4b0a25b5e15afb041e to your computer and use it in GitHub Desktop.
usePersistentState LocalStorage/SessionStorage #react #hook
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
import React, { Dispatch, SetStateAction } from "react"; | |
type ViolationMessage = string | (() => string); | |
function useStableValue<V>( | |
value: V, | |
message: ViolationMessage = "Violation: cannot change value between re-renders", | |
) { | |
const ref = React.useRef(value); | |
if (process.env.NODE_ENV !== "production") { | |
if (ref.current !== value) { | |
throw new Error(typeof message === "function" ? message() : message); | |
} | |
} | |
return ref.current; | |
} | |
/** | |
* Similar to `React.useState`, but persists the state in a `Storage` (typically | |
* either LocalStorage or SessionStorage). If `enabled` is set to `false` the | |
* hook behaves **exactly** like `useState`. | |
* | |
* @param enabled Enable the persistence conditionally. | |
* **Must never change during re-renders.** | |
* | |
* @param storageKey The key which will be used to store the state in the | |
* storage. **Must never change during re-renders.** | |
* | |
* @param initialState | |
* | |
* @param storage The storage to persist to. You can simply pass `localStorage` | |
* or `sessionStorage`. **Must never change during re-renders.** Defaults to | |
* `localStorage`. | |
*/ | |
export default function usePersistentState<S>( | |
enabled: boolean, | |
storageKey: string, | |
initialState: S | (() => S), | |
storage: Storage = localStorage, | |
): [S, Dispatch<SetStateAction<S>>] { | |
enabled = useStableValue( | |
enabled, | |
"usePersistentState: 'storageKey' changed between re-renders", | |
); | |
storageKey = useStableValue( | |
storageKey, | |
"usePersistentState: 'storageKey' changed between re-renders", | |
); | |
storage = useStableValue( | |
storage, | |
"usePersistentState: 'storage' changed between re-renders", | |
); | |
const [state, setState] = React.useState<S>( | |
enabled | |
? () => { | |
const stored = storage.getItem(storageKey); | |
if (stored !== null) { | |
try { | |
return JSON.parse(stored); | |
} catch (error) { | |
console.error(`Cannot parse '${storageKey}':`, error); | |
} | |
} | |
return typeof initialState === "function" | |
? (initialState as any)() | |
: initialState; | |
} | |
: initialState, | |
); | |
React.useEffect(() => { | |
if (enabled) { | |
const serialized = JSON.stringify(state); | |
const stored = storage.getItem(storageKey); | |
if (serialized !== stored) { | |
storage.setItem(storageKey, JSON.stringify(state)); | |
} | |
} | |
}, [enabled, storageKey, storage, state]); | |
React.useEffect(() => { | |
function listener(event: StorageEvent) { | |
if (event.key === storageKey && event.storageArea === storage) { | |
if (event.newValue !== null) { | |
try { | |
setState(JSON.parse(event.newValue)); | |
} catch (error) { | |
console.error(`Cannot synchronize '${storageKey}':`, error); | |
} | |
} else { | |
// TODO: We are not resetting to initialState to not have to add it as a dependency | |
} | |
} | |
} | |
if (storageKey !== null) { | |
window.addEventListener("storage", listener); | |
return function cleanup() { | |
window.removeEventListener("storage", listener); | |
}; | |
} else { | |
return; | |
} | |
}, [enabled, storageKey, storage]); | |
return [state, setState]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment