Last active
March 24, 2021 20:32
-
-
Save dmytrosydor3nk0/12cfe5d8d89f4dbe147c58cf230031ab to your computer and use it in GitHub Desktop.
Local Storage React Hook (Typescript)
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, { | |
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]; | |
}; |
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 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> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Codesandbox:
https://codesandbox.io/s/react-hook-local-storage-jwlrk
Demo:
https://jwlrk.csb.app