Skip to content

Instantly share code, notes, and snippets.

@benhorst
Last active May 24, 2020 02:17
Show Gist options
  • Save benhorst/aa04c48b6934f94730b0716675f28329 to your computer and use it in GitHub Desktop.
Save benhorst/aa04c48b6934f94730b0716675f28329 to your computer and use it in GitHub Desktop.
simple localstorage sync script with comments and notes
import { useEffect, useState } from "react";
// a quick example of "enhancing" the localstorage sync
// in order to use it like a specialized store.
// you're obviously going to get worse perf than a regular React.createContext, but you get some benefits.
const validDetails = ["name", "favColor"];
export const usePlayerDetail = (key) => {
// check for valid inputs, ensure we're only using keys we care about.
// potential migration concerns
if (!validDetails.includes(key)) {
console.error(
`Invalid key provided to usePlayerDetail: ${key}. Valid keys: ${validDetails}.`
);
}
const playerKey = "player." + key;
return useLocalStorage(playerKey, "");
};
const identity = (x) => x;
const defaultTransform = { serialize: identity, deserialize: identity };
// if you need to save an object or array, you want to use this. perf hit depending on object size.
export const objectTransform = {
serialize: (ob) => JSON.stringify(ob),
deserialize: (s) => JSON.parse(s),
};
// this hook assumes you want to store a string. if you need more than that, use transforms for json or look elsewhere
// the only required parameter in that case is "key". You'll get a standard "useState" interface from there.
export const useLocalStorage = (
key,
defaultValue = "",
transform = defaultTransform
) => {
// TODO: assert(transform.serialize is a function) && assert(transform.deserialize is a function);
// keep things safe for server.
// since this uses localstorage, we need to useEffect to get a client-side state only
const [initialized, setInitialized] = useState(false);
useEffect(() => {
setInitialized(true);
// once we're initialized and on the client, we can set up the state.
setValue(
transform.serialize(window.localStorage.getItem(key)) || defaultValue
);
}, [typeof window]);
// ok now that we're safe from server bs, do the thing.
const [value, setValue] = useState(defaultValue);
useEffect(() => {
// check on storage event if this is what we care about.
const updateFromStorage = (ev) => {
// ev = { newValue, oldValue, key, ... }; reference: https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent
if (
// is this a key I care about
key === ev.key &&
// prevent infinite loops
ev.newValue !== value
) {
setValue(transform.deserialize(ev.newValue));
}
};
// subscribe to localstorage events
window.addEventListener("storage", updateFromStorage);
return () => window.removeEventListener("storage", updateFromStorage);
});
// to make sure we're not double-binding to the "value"
// we need to utilize an internal update pattern
const externalSetValue = (v) => {
// update storage
window.localStorage.setItem(key, v);
// update our version
setValue(v);
};
// expose the state+setter pair the same as a "useState"
return [value, externalSetValue];
};
@benhorst
Copy link
Author

Butts. This one doesn't quite work without a Provider. These hooks do a good job syncing cross tab through storage events, but you cannot have 2 on the same page -- they won't sync. If you use it ONCE and send the value to a provider, that'll do it.

EXAMPLE -- wrap your components in this ONCE:

const [value, setValue] = useLocalStorage(false);

return (<SomethingProvider value={value}>{ children }</SomethingProvider>);```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment