diff options
Diffstat (limited to 'apps/web/src/services/storage/use-local-storage.ts')
| -rw-r--r-- | apps/web/src/services/storage/use-local-storage.ts | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/apps/web/src/services/storage/use-local-storage.ts b/apps/web/src/services/storage/use-local-storage.ts new file mode 100644 index 00000000..d0188ab3 --- /dev/null +++ b/apps/web/src/services/storage/use-local-storage.ts @@ -0,0 +1,58 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +export function useLocalStorage<T>({ + key, + defaultValue, +}: { + key: string; + defaultValue: T; +}): [ + T, + ({ value }: { value: T | ((previousValue: T) => T) }) => void, + boolean, +] { + const [value, setValue] = useState<T>(defaultValue); + const [isReady, setIsReady] = useState(false); + const valueRef = useRef(defaultValue); + + // avoid hydration mismatch by reading after mount + useEffect(() => { + try { + const storedValue = localStorage.getItem(key); + if (storedValue !== null) { + const parsedValue = JSON.parse(storedValue) as T; + valueRef.current = parsedValue; + setValue(parsedValue); + } + } catch { + // localstorage might be unavailable + } + setIsReady(true); + }, [key]); + + // sync to localstorage after hydration + useEffect(() => { + if (!isReady) return; + + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // localstorage might be full or disabled + } + }, [key, value, isReady]); + + const setValueWithCallback = useCallback( + ({ value: nextValue }: { value: T | ((previousValue: T) => T) }) => { + const resolvedValue = + typeof nextValue === "function" + ? (nextValue as (previousValue: T) => T)(valueRef.current) + : nextValue; + + valueRef.current = resolvedValue; + setValue(resolvedValue); + }, + [], + ); + + return [value, setValueWithCallback, isReady]; +} |
