Skip to content

Instantly share code, notes, and snippets.

@freddi301
Created October 11, 2019 09:59
Show Gist options
  • Save freddi301/c71c53920fa45b4b0a25b5e15afb041e to your computer and use it in GitHub Desktop.
Save freddi301/c71c53920fa45b4b0a25b5e15afb041e to your computer and use it in GitHub Desktop.
usePersistentState LocalStorage/SessionStorage #react #hook
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