Skip to content

Instantly share code, notes, and snippets.

@dmytrosydor3nk0
Last active March 24, 2021 20:32
Show Gist options
  • Save dmytrosydor3nk0/12cfe5d8d89f4dbe147c58cf230031ab to your computer and use it in GitHub Desktop.
Save dmytrosydor3nk0/12cfe5d8d89f4dbe147c58cf230031ab to your computer and use it in GitHub Desktop.
Local Storage React Hook (Typescript)
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from "react";
/**
* Json type by indiescripter (https://github.com/indiescripter)
* https://github.com/microsoft/TypeScript/issues/1897#issuecomment-338650717
* Thanks!
*/
type AnyJson = boolean | number | string | null | JsonArray | JsonMap;
interface JsonMap {
[key: string]: AnyJson;
}
interface JsonArray extends Array<AnyJson> {}
export interface ILocalStorageCtx {
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void;
removeItem: (key: string) => void;
onChange: (cb: () => void, key?: string) => () => void;
}
export const defaultCtx: ILocalStorageCtx = {
getItem: localStorage.getItem,
setItem: localStorage.setItem,
removeItem: localStorage.removeItem,
onChange: () => {
return () => {};
}
};
export const ctx = React.createContext<ILocalStorageCtx>(defaultCtx);
export const Provider = ({ children }: React.PropsWithChildren<{}>) => {
const callbacks = useRef<Array<Function>>([]);
const keyCallbacks = useRef<Record<string, Array<Function>>>({});
const getItem = useCallback((key: string): string | null => {
return localStorage.getItem(key);
}, []);
const setItem = useCallback((key: string, value: string): void => {
localStorage.setItem(key, value);
callbacks.current.forEach((cb) => cb());
(keyCallbacks.current[key] || []).forEach((cb) => cb());
}, []);
const removeItem = useCallback((key: string): void => {
localStorage.removeItem(key);
callbacks.current.forEach((cb) => cb());
(keyCallbacks.current[key] || []).forEach((cb) => cb());
}, []);
const onChange = useCallback<ILocalStorageCtx["onChange"]>((cb, key) => {
if (key !== undefined) {
if (!keyCallbacks.current[key]) {
keyCallbacks.current[key] = [];
}
if (keyCallbacks.current[key].indexOf(cb) === -1) {
keyCallbacks.current[key].push(cb);
}
return () => {
const index = (keyCallbacks.current[key] || []).findIndex(
(v) => v === cb
);
if (index > -1) {
keyCallbacks.current[key].splice(index, 1);
}
};
} else {
if (callbacks.current.indexOf(cb) === -1) {
callbacks.current.push(cb);
}
return () => {
const index = callbacks.current.findIndex((v) => v === cb);
if (index > -1) {
callbacks.current.splice(index, 1);
}
};
}
}, []);
const newCtx = useMemo<ILocalStorageCtx>(() => {
return {
getItem,
setItem,
removeItem,
onChange
};
}, [getItem, setItem, removeItem, onChange]);
return <ctx.Provider value={newCtx}>{children}</ctx.Provider>;
};
export const useLocalStorageItem = <T extends AnyJson>(
key: string,
deserialize: (key: string | null) => T,
serialize: (key: T) => string
): [T, (value: T) => void, () => void] => {
const localStorage = useContext(ctx);
const startVarlueStr = useMemo(() => localStorage.getItem(key), [
localStorage,
key
]);
const [str, setStr] = useState(startVarlueStr);
useEffect(() => {
setStr(startVarlueStr);
}, [startVarlueStr]);
useEffect(() => {
const v = localStorage.getItem(key);
setStr(v);
const cb = () => {
const newV = localStorage.getItem(key);
setStr(newV);
};
const unlisten = localStorage.onChange(cb, key);
return () => {
unlisten();
};
}, [localStorage, key, setStr]);
const value = useMemo(() => deserialize(str), [str, deserialize]);
const updateItem = useCallback(
(newValue) => {
const newValueStr = serialize(newValue);
localStorage.setItem(key, newValueStr);
},
[localStorage, key, serialize]
);
const removeItem = useCallback(() => {
localStorage.removeItem(key);
}, [localStorage, key]);
return [value, updateItem, removeItem];
};
import React from "react";
import { useLocalStorageItem } from "./LocalStorageHook";
const useTestKey = (key: string = "test-key") => {
const serialize = React.useCallback((v: string[]) => JSON.stringify(v), []);
const deserialize = React.useCallback((v: string | null) => {
return v !== null ? (JSON.parse(v) as string[]) : [];
}, []);
return useLocalStorageItem(key, deserialize, serialize);
};
const Buttons = ({ storageKey }: { storageKey: string }) => {
const [value, update, remove] = useTestKey(storageKey);
return (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<button onClick={remove}>Clear all</button>
<button
onClick={() => {
const newKey = "element " + (value.length + 1);
update([newKey, ...value]);
}}
>
Add item
</button>
</div>
);
};
export function Example({
storageKey,
onChange,
onRemove
}: {
storageKey: string;
onChange: (key: string) => void;
onRemove: () => void;
}) {
const [value, update] = useTestKey(storageKey);
React.useEffect(() => {
console.log("Debug:", value);
}, [value]);
const removeItem = React.useCallback(
(i: number) => {
update(value.filter((_, idx) => idx !== i));
},
[value, update]
);
return (
<div
style={{
display: "flex",
flexDirection: "column",
padding: 10,
margin: 5,
border: "1px solid lightgray",
borderRadius: 4,
background: "#ededed",
width: 170,
height: 300,
flexGrow: 0
}}
>
<div style={{ textAlign: "right" }}>
<button onClick={onRemove}>x</button>
</div>
<h2>{storageKey}</h2>
<input value={storageKey} onChange={(e) => onChange(e.target.value)} />
<br />
<Buttons storageKey={storageKey} />
<div style={{ padding: "5px 0", flexGrow: 1, overflow: "auto" }}>
{value.map((str, i) => (
<div key={i} style={{ marginTop: 5 }}>
<button onClick={() => removeItem(i)}>-</button> {str}
</div>
))}
</div>
</div>
);
}
export default function App() {
const [arr, setArr] = useTestKey("keys-on-page");
const updateKey = React.useCallback(
(i: number, key: string) => {
setArr(arr.map((k, idx) => (idx === i ? key : k)));
},
[arr, setArr]
);
const onNewItem = React.useCallback(() => {
const newKey = "test-key-" + (arr.length + 1);
setArr([...arr, newKey]);
}, [arr, setArr]);
return (
<div className="App">
<div
style={{
display: "flex",
flexWrap: "wrap"
}}
>
{arr.map((storageKey, i) => (
<Example
storageKey={storageKey}
onChange={(val) => updateKey(i, val)}
onRemove={() => setArr(arr.filter((_, idx) => i !== idx))}
key={i}
/>
))}
<button
onClick={onNewItem}
style={{
width: 170,
height: 300,
padding: 10,
margin: 5,
fontSize: "22px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
border: "1px dashed gray",
borderRadius: 4,
background: "#ededed"
}}
>
<div style={{ fontSize: "64px", lineHeight: "44px" }}>+</div>
New Block
</button>
</div>
</div>
);
}
@dmytrosydor3nk0
Copy link
Author

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