-
-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathindex.ts
174 lines (157 loc) · 6.02 KB
/
index.ts
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import { useState, useEffect, useCallback, Dispatch, SetStateAction } from 'react'
/**
* Abstraction for localStorage that uses an in-memory fallback when localStorage throws an error.
* Reasons for throwing an error:
* - maximum quota is exceeded
* - under Mobile Safari (since iOS 5) when the user enters private mode `localStorage.setItem()`
* will throw
* - trying to access localStorage object when cookies are disabled in Safari throws
* "SecurityError: The operation is insecure."
*/
const data: Record<string, unknown> = {}
const storage = {
get<T>(key: string, defaultValue: T): T {
try {
return data[key] ?? JSON.parse(localStorage.getItem(key) ?? '')
} catch {
return defaultValue
}
},
set<T>(key: string, value: T): boolean {
try {
localStorage.setItem(key, JSON.stringify(value))
data[key] = undefined
return true
} catch {
data[key] = value
return false
}
},
}
/**
* Used to track usages of `useLocalStorageState()` with identical `key` values. If we encounter
* duplicates we throw an error to the user telling them to use `createLocalStorageStateHook`
* instead.
*/
const initializedStorageKeys = new Set<string>()
type SetStateParameter<T> = T | undefined | ((value: T | undefined) => T | undefined)
export default function useLocalStorageState<T = undefined>(
key: string,
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, boolean]
export default function useLocalStorageState<T>(
key: string,
defaultValue: T | (() => T),
): [T, Dispatch<SetStateAction<T>>, boolean]
export default function useLocalStorageState<T = undefined>(
key: string,
defaultValue?: T | (() => T),
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, boolean] {
const [state, setState] = useState(() => {
const isCallable = (value: unknown): value is () => T => typeof value === 'function'
return {
isPersistent: (() => {
/**
* We want to return `true` on the server. If you render a message based on `isPersistent` and the
* server returns `false` then the message will flicker until hydration is done:
* `{!isPersistent && <span>You changes aren't being persisted.</span>}`
*/
if (typeof window === 'undefined') {
return true
}
try {
localStorage.setItem('--use-local-storage-state--', 'dummy')
localStorage.removeItem('--use-local-storage-state--')
return true
} catch {
return false
}
})(),
value: isCallable(defaultValue)
? storage.get(key, defaultValue())
: storage.get(key, defaultValue),
}
})
const updateValue = useCallback(
(newValue: SetStateParameter<T>) => {
setState((state) => {
const isCallable = (
value: unknown,
): value is (value: T | undefined) => T | undefined => typeof value === 'function'
const value = isCallable(newValue) ? newValue(state.value) : newValue
return {
value: value,
isPersistent: storage.set(key, value),
}
})
},
[key],
)
/**
* Detects incorrect usage of the library and throws an error with a suggestion how to fix it.
*/
useEffect(() => {
if (initializedStorageKeys.has(key)) {
throw new Error(
`Multiple instances of useLocalStorageState() initialized with the same key. ` +
`Use createLocalStorageStateHook() instead. ` +
`Look at the example here: ` +
`https://github.com/astoilkov/use-local-storage-state#create-local-storage-state-hook-example`,
)
} else {
initializedStorageKeys.add(key)
}
return () => void initializedStorageKeys.delete(key)
}, [])
/**
* Syncs changes across tabs and iframe's.
*/
useEffect(() => {
const onStorage = (e: StorageEvent): void => {
if (e.storageArea === localStorage && e.key === key) {
setState({
isPersistent: true,
value: e.newValue === null ? defaultValue : JSON.parse(e.newValue),
})
}
}
window.addEventListener('storage', onStorage)
return (): void => window.removeEventListener('storage', onStorage)
}, [])
return [state.value, updateValue, state.isPersistent]
}
export function createLocalStorageStateHook<T = undefined>(
key: string,
): () => [T | undefined, Dispatch<SetStateAction<T | undefined>>, boolean]
export function createLocalStorageStateHook<T>(
key: string,
defaultValue: T | (() => T),
): () => [T, Dispatch<SetStateAction<T>>, boolean]
export function createLocalStorageStateHook<T>(
key: string,
defaultValue?: T | (() => T),
): () => [T | undefined, Dispatch<SetStateAction<T | undefined>>, boolean] {
const updates: ((newValue: SetStateParameter<T>) => void)[] = []
return function useLocalStorageStateHook(): [
T | undefined,
Dispatch<SetStateAction<T | undefined>>,
boolean,
] {
const [value, setValue, isPersistent] = useLocalStorageState<T | undefined>(
key,
defaultValue,
)
const updateValue = useCallback((newValue: SetStateParameter<T>) => {
for (const update of updates) {
update(newValue)
}
}, [])
useEffect(() => {
initializedStorageKeys.delete(key)
}, [])
useEffect(() => {
updates.push(setValue)
return () => void updates.splice(updates.indexOf(setValue), 1)
}, [setValue])
return [value, updateValue, isPersistent]
}
}