useLocalStorage()

Persist the state with local storage so that it remains after a page refresh. This can be useful for a dark theme. This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. If the window object is not present (as in SSR), useLocalStorage() will return the default value.

Side notes:

Related hooks:

The Hook

1import {
2 Dispatch,
3 SetStateAction,
4 useCallback,
5 useEffect,
6 useState,
7} from 'react'
8
9import { useEventCallback, useEventListener } from 'usehooks-ts'
10
11declare global {
12 interface WindowEventMap {
13 'local-storage': CustomEvent
14 }
15}
16
17type SetValue<T> = Dispatch<SetStateAction<T>>
18
19function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
20 // Get from local storage then
21 // parse stored json or return initialValue
22 const readValue = useCallback((): T => {
23 // Prevent build error "window is undefined" but keeps working
24 if (typeof window === 'undefined') {
25 return initialValue
26 }
27
28 try {
29 const item = window.localStorage.getItem(key)
30 return item ? (parseJSON(item) as T) : initialValue
31 } catch (error) {
32 console.warn(`Error reading localStorage key “${key}”:`, error)
33 return initialValue
34 }
35 }, [initialValue, key])
36
37 // State to store our value
38 // Pass initial state function to useState so logic is only executed once
39 const [storedValue, setStoredValue] = useState<T>(readValue)
40
41 // Return a wrapped version of useState's setter function that ...
42 // ... persists the new value to localStorage.
43 const setValue: SetValue<T> = useEventCallback(value => {
44 // Prevent build error "window is undefined" but keeps working
45 if (typeof window === 'undefined') {
46 console.warn(
47 `Tried setting localStorage key “${key}” even though environment is not a client`,
48 )
49 }
50
51 try {
52 // Allow value to be a function so we have the same API as useState
53 const newValue = value instanceof Function ? value(storedValue) : value
54
55 // Save to local storage
56 window.localStorage.setItem(key, JSON.stringify(newValue))
57
58 // Save state
59 setStoredValue(newValue)
60
61 // We dispatch a custom event so every useLocalStorage hook are notified
62 window.dispatchEvent(new Event('local-storage'))
63 } catch (error) {
64 console.warn(`Error setting localStorage key “${key}”:`, error)
65 }
66 })
67
68 useEffect(() => {
69 setStoredValue(readValue())
70 // eslint-disable-next-line react-hooks/exhaustive-deps
71 }, [])
72
73 const handleStorageChange = useCallback(
74 (event: StorageEvent | CustomEvent) => {
75 if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
76 return
77 }
78 setStoredValue(readValue())
79 },
80 [key, readValue],
81 )
82
83 // this only works for other documents, not the current one
84 useEventListener('storage', handleStorageChange)
85
86 // this is a custom event, triggered in writeValueToLocalStorage
87 // See: useLocalStorage()
88 useEventListener('local-storage', handleStorageChange)
89
90 return [storedValue, setValue]
91}
92
93export default useLocalStorage
94
95// A wrapper for "JSON.parse()"" to support "undefined" value
96function parseJSON<T>(value: string | null): T | undefined {
97 try {
98 return value === 'undefined' ? undefined : JSON.parse(value ?? '')
99 } catch {
100 console.log('parsing error on', { value })
101 return undefined
102 }
103}

Usage

1import { useLocalStorage } from 'usehooks-ts'
2
3// Usage
4export default function Component() {
5 const [isDarkTheme, setDarkTheme] = useLocalStorage('darkTheme', true)
6
7 const toggleTheme = () => {
8 setDarkTheme((prevValue: boolean) => !prevValue)
9 }
10
11 return (
12 <button onClick={toggleTheme}>
13 {`The current theme is ${isDarkTheme ? `dark` : `light`}`}
14 </button>
15 )
16}

Edit on CodeSandbox

See a way to make this page better?
Edit there »