summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/use-local-storage.ts
blob: d0188ab3203cc88537a6bc61c7f82b82869b83a0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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];
}