Skip to content

Instantly share code, notes, and snippets.

@vincentriemer
Created August 6, 2019 15:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vincentriemer/1dae699bc126b0d2f20dc435324e0d63 to your computer and use it in GitHub Desktop.
Save vincentriemer/1dae699bc126b0d2f20dc435324e0d63 to your computer and use it in GitHub Desktop.
My take on a type-safe usePersistedState hook, leveraging React suspense/concurrent-mode, DOM custom events, and the kv-storage builtin module.
/**
* @flow
*/
import * as React from "react";
import { unstable_createResource } from "react-cache";
import * as Scheduler from "scheduler";
import { StorageArea } from "std:kv-storage";
// Creates a ref that tracks the latest value of the argument passed to it
import { useLatestValueRef } from "~/Hooks/useLatestValueRef";
const STORAGE_EVENT_NAME = (persistKey: string) =>
`peristed-state-storage:${persistKey}`;
const storage = new StorageArea("react-persisted-state");
const storageResource: {| read: string => mixed |} = unstable_createResource(
async (key: string): Promise<mixed> => {
return await storage.get(key);
}
);
const persistedStateMemoryStore: { [key: string]: mixed } = {};
function dispatchStorageEvent(name: string, state: mixed): void {
const storageEvent = new CustomEvent(name, { detail: state });
document.dispatchEvent(storageEvent);
}
// type constraint that enforces json-compatible values
type JSONValue =
| string
| number
| boolean
| null
| { [string]: JSONValue }
| Array<JSONValue>;
// state updater function that has the same shape as React.useState's
type StateUpdater<T: JSONValue> = (nextState: (T => T) | T) => void;
// validator that takes in an unknown value from the store and returns
// the expected type if it validates and undefined otherwise
type StateValidator<T: JSONValue> = mixed => T | void;
function usePersistedState<T: JSONValue>(
defaultValue: T | (() => T),
validatePersistedValue: StateValidator<T>,
persistKey: string
): [T, StateUpdater<T>] {
const memoryInitialState = persistedStateMemoryStore[persistKey];
const storageInitialState = storageResource.read(persistKey);
const [initialStateFactory] = React.useState(
(): T | (() => T) => {
// get the inital state in priority order: Memory -> Storage -> Default
const validatedMemory = validatePersistedValue(memoryInitialState);
if (validatedMemory !== undefined) {
return validatedMemory;
}
const validatedStorage = validatePersistedValue(storageInitialState);
if (validatedStorage !== undefined) {
return validatedStorage;
}
return defaultValue;
}
);
const [state, updateState] = React.useState(initialStateFactory);
const stateRef = useLatestValueRef(state);
const persistKeyRef = useLatestValueRef(persistKey);
const stateValidatorRef = useLatestValueRef(validatePersistedValue);
const stateUpdater: StateUpdater<T> = React.useCallback(
nextState => {
// resolve the next state
let newState = null;
if (typeof nextState === "function") {
const currentState = stateRef.current;
newState = nextState(currentState);
} else {
newState = nextState;
}
// update current instance's state
updateState(newState);
// update memory store
persistedStateMemoryStore[persistKeyRef.current] = newState;
// send event out to update other instances
dispatchStorageEvent(STORAGE_EVENT_NAME(persistKeyRef.current), newState);
// update the persisted store at a lower priority
Scheduler.unstable_scheduleCallback(
Scheduler.unstable_LowPriority,
() => {
storage.set(persistKeyRef.current, newState);
}
);
},
[persistKeyRef, stateRef]
);
// $FlowFixMe - Custom Events not typed in flow lib core
const handleStateChangeFromOtherInstance: EventListener = React.useCallback(
({ detail }: { detail: mixed }) => {
const newState = stateValidatorRef.current(detail);
// don't update the state if the incoming value is undefined or the current value.
// this is important because the instance will recieve state update events from itself
// and we want to avoid a double state update
if (newState !== undefined && newState !== stateRef.current) {
updateState(newState);
}
},
[stateRef, stateValidatorRef]
);
// listen for updates from other instances
React.useEffect(() => {
const eventName = STORAGE_EVENT_NAME(persistKeyRef.current);
document.addEventListener(
eventName,
handleStateChangeFromOtherInstance,
false
);
return () => {
document.removeEventListener(
eventName,
handleStateChangeFromOtherInstance,
false
);
};
}, [
handleStateChangeFromOtherInstance,
persistKeyRef,
stateRef,
stateValidatorRef
]);
return [state, stateUpdater];
}
export { usePersistedState };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment