useLocalStorage.ts113 lines · main
1// Reference: https://usehooks.com/useLocalStorage/
2
3import { useQuery, useQueryClient } from '@tanstack/react-query'
4import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'
5
6export function useLocalStorage<T>(key: string, initialValue: T) {
7 // State to store our value
8 // Pass initial state function to useState so logic is only executed once
9 const [storedValue, setStoredValue] = useState<T>(() => {
10 if (typeof window === 'undefined') {
11 return initialValue
12 }
13
14 try {
15 // Get from local storage by key
16 const item = window.localStorage.getItem(key)
17 // Parse stored json or if none return initialValue
18 return item ? JSON.parse(item) : initialValue
19 } catch (error) {
20 // If error also return initialValue
21 console.log(error)
22 return initialValue
23 }
24 })
25
26 // Return a wrapped version of useState's setter function that ...
27 // ... persists the new value to localStorage.
28 const setValue = useCallback(
29 (value: T | ((val: T) => T)) => {
30 try {
31 // Allow value to be a function so we have same API as useState
32 const valueToStore = value instanceof Function ? value(storedValue) : value
33 // Save state
34 setStoredValue(valueToStore)
35 // Save to local storage
36 if (typeof window !== 'undefined') {
37 window.localStorage.setItem(key, JSON.stringify(valueToStore))
38 }
39 } catch (error) {
40 // A more advanced implementation would handle the error case
41 console.log(error)
42 }
43 },
44 [key, storedValue]
45 )
46
47 return [storedValue, setValue] as const
48}
49
50/**
51 * Hook to load/store values from local storage with an API similar
52 * to `useState()`.
53 *
54 * Differs from `useLocalStorage()` in that it uses `react-query` to
55 * invalidate stale values across hooks with the same key.
56 */
57export function useLocalStorageQuery<T>(key: string, initialValue: T) {
58 const queryClient = useQueryClient()
59 const queryKey = useMemo(() => ['localStorage', key], [key])
60
61 const {
62 error,
63 data: storedValue = initialValue,
64 isSuccess,
65 isLoading,
66 isError,
67 } = useQuery({
68 queryKey,
69 queryFn: () => {
70 if (typeof window === 'undefined') {
71 return initialValue
72 }
73
74 const item = window.localStorage.getItem(key)
75
76 if (!item) {
77 return initialValue
78 }
79
80 return JSON.parse(item) as T
81 },
82 })
83
84 const setValue: Dispatch<SetStateAction<T>> = useCallback(
85 (value) => {
86 const currentValue = queryClient.getQueryData<T>(queryKey) ?? initialValue
87 const valueToStore = value instanceof Function ? value(currentValue) : value
88
89 // Bail out when the value is unchanged (matches useState semantics).
90 // Without this, no-op updates from consumers — like a pruning effect
91 // whose updater returns `current` unchanged — still write to
92 // localStorage and invalidate the query, which churns subscribers and
93 // can cascade into "Maximum update depth exceeded" when two consumers
94 // of the same key are mounted together.
95 if (Object.is(valueToStore, currentValue)) return
96
97 if (typeof window !== 'undefined') {
98 window.localStorage.setItem(key, JSON.stringify(valueToStore))
99 }
100
101 queryClient.setQueryData(queryKey, valueToStore)
102 queryClient.invalidateQueries({ queryKey })
103 },
104 // initialValue is intentionally excluded: the function body reads the
105 // current value via queryClient.getQueryData so it doesn't close over
106 // initialValue reactively — including it would cause a new function
107 // reference every render when callers pass an inline literal (e.g. []).
108 // eslint-disable-next-line react-hooks/exhaustive-deps
109 [key, queryKey, queryClient]
110 )
111
112 return [storedValue, setValue, { isSuccess, isLoading, isError, error }] as const
113}