( key: string, initialValue: T, options?: UsePersistedStateOptions )
| 223 | * @returns [state, setState] tuple matching useState API |
| 224 | */ |
| 225 | export function usePersistedState<T>( |
| 226 | key: string, |
| 227 | initialValue: T, |
| 228 | options?: UsePersistedStateOptions |
| 229 | ): [T, Dispatch<SetStateAction<T>>] { |
| 230 | // Unique component ID for distinguishing self-updates. |
| 231 | const componentIdRef = useRef(Math.random().toString(36)); |
| 232 | |
| 233 | ensureStorageListenerInstalled(); |
| 234 | |
| 235 | const subscribe = useCallback( |
| 236 | (callback: () => void) => { |
| 237 | return addSubscriber(key, { |
| 238 | callback, |
| 239 | componentId: componentIdRef.current, |
| 240 | listener: Boolean(options?.listener), |
| 241 | }); |
| 242 | }, |
| 243 | [key, options?.listener] |
| 244 | ); |
| 245 | |
| 246 | // Match the previous `usePersistedState` behavior: `initialValue` is only used |
| 247 | // as the default when no value is stored; changes to `initialValue` should not |
| 248 | // reinitialize state. |
| 249 | const initialValueRef = useRef(initialValue); |
| 250 | |
| 251 | // useSyncExternalStore requires getSnapshot() to be referentially stable when |
| 252 | // the underlying store value is unchanged. Since localStorage values are JSON, |
| 253 | // we cache the parsed value by raw string. |
| 254 | const snapshotRef = useRef<{ key: string; raw: string | null; value: T } | null>(null); |
| 255 | |
| 256 | const getSnapshot = useCallback((): T => { |
| 257 | if (typeof window === "undefined" || !window.localStorage) { |
| 258 | return initialValueRef.current; |
| 259 | } |
| 260 | |
| 261 | try { |
| 262 | const raw = window.localStorage.getItem(key); |
| 263 | |
| 264 | if (raw === null || raw === "undefined") { |
| 265 | if (snapshotRef.current?.key === key && snapshotRef.current.raw === null) { |
| 266 | return snapshotRef.current.value; |
| 267 | } |
| 268 | |
| 269 | snapshotRef.current = { |
| 270 | key, |
| 271 | raw: null, |
| 272 | value: initialValueRef.current, |
| 273 | }; |
| 274 | |
| 275 | return initialValueRef.current; |
| 276 | } |
| 277 | |
| 278 | if (snapshotRef.current?.key === key && snapshotRef.current.raw === raw) { |
| 279 | return snapshotRef.current.value; |
| 280 | } |
| 281 | |
| 282 | const parsed = JSON.parse(raw) as T; |
no test coverage detected