Skip to content

Commit b091404

Browse files
committed
feat: add useLocalStorage hook for managing local storage state with customizable serialization and deserialization options
1 parent bea276f commit b091404

File tree

1 file changed

+119
-0
lines changed

1 file changed

+119
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
type Dispatch,
3+
type SetStateAction,
4+
useCallback,
5+
useLayoutEffect,
6+
useRef,
7+
useState,
8+
} from 'preact/compat'
9+
import { isBrowser, noop } from './misc/util'
10+
11+
type parserOptions<T> =
12+
| {
13+
raw: true
14+
}
15+
| {
16+
raw: false
17+
serializer: (value: T) => string
18+
deserializer: (value: string) => T
19+
}
20+
21+
export const useLocalStorage = <T>(
22+
key: string,
23+
initialValue?: T,
24+
options?: parserOptions<T>,
25+
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => {
26+
if (!isBrowser) {
27+
return [initialValue as T, noop, noop]
28+
}
29+
if (!key) {
30+
throw new Error('useLocalStorage key may not be falsy')
31+
}
32+
33+
const deserializer = options
34+
? options.raw
35+
? (value: string) => value
36+
: options.deserializer
37+
: JSON.parse
38+
39+
// eslint-disable-next-line react-hooks/rules-of-hooks
40+
// biome-ignore lint/correctness/useHookAtTopLevel: x
41+
const initializer = useRef((key: string) => {
42+
try {
43+
const serializer = options
44+
? options.raw
45+
? String
46+
: options.serializer
47+
: JSON.stringify
48+
49+
const localStorageValue = localStorage.getItem(key)
50+
if (localStorageValue !== null) {
51+
return deserializer(localStorageValue)
52+
} else {
53+
initialValue && localStorage.setItem(key, serializer(initialValue))
54+
return initialValue
55+
}
56+
} catch {
57+
// If user is in private mode or has storage restriction
58+
// localStorage can throw. JSON.parse and JSON.stringify
59+
// can throw, too.
60+
return initialValue
61+
}
62+
})
63+
64+
// eslint-disable-next-line react-hooks/rules-of-hooks
65+
// biome-ignore lint/correctness/useHookAtTopLevel: x
66+
const [state, setState] = useState<T | undefined>(() =>
67+
initializer.current(key),
68+
)
69+
70+
// eslint-disable-next-line react-hooks/rules-of-hooks
71+
// biome-ignore lint/correctness/useHookAtTopLevel: x
72+
useLayoutEffect(() => setState(initializer.current(key)), [key])
73+
74+
// eslint-disable-next-line react-hooks/rules-of-hooks
75+
// biome-ignore lint/correctness/useExhaustiveDependencies: x
76+
// biome-ignore lint/correctness/useHookAtTopLevel: x
77+
const set: Dispatch<SetStateAction<T | undefined>> = useCallback(
78+
(valOrFunc) => {
79+
try {
80+
const newState =
81+
typeof valOrFunc === 'function'
82+
? (valOrFunc as Function)(state)
83+
: valOrFunc
84+
if (typeof newState === 'undefined') return
85+
let value: string
86+
87+
if (options)
88+
if (options.raw)
89+
if (typeof newState === 'string') value = newState
90+
else value = JSON.stringify(newState)
91+
else if (options.serializer) value = options.serializer(newState)
92+
else value = JSON.stringify(newState)
93+
else value = JSON.stringify(newState)
94+
95+
localStorage.setItem(key, value)
96+
setState(deserializer(value))
97+
} catch {
98+
// If user is in private mode or has storage restriction
99+
// localStorage can throw. Also JSON.stringify can throw.
100+
}
101+
},
102+
[key, setState],
103+
)
104+
105+
// eslint-disable-next-line react-hooks/rules-of-hooks
106+
// biome-ignore lint/correctness/useExhaustiveDependencies: x
107+
// biome-ignore lint/correctness/useHookAtTopLevel: x
108+
const remove = useCallback(() => {
109+
try {
110+
localStorage.removeItem(key)
111+
setState(undefined)
112+
} catch {
113+
// If user is in private mode or has storage restriction
114+
// localStorage can throw.
115+
}
116+
}, [key, setState])
117+
118+
return [state, set, remove]
119+
}

0 commit comments

Comments
 (0)