custom hooks

useLocalStorage( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The custom hook useLocalStorage serves as a versatile tool for managing localStorage in React applications. Designed to streamline data persistence across sessions, it encapsulates common operations like storing, updating, and removing data with ease.

At its core, this hook leverages React's state management capabilities through the useState hook, initializing state based on stored values retrieved from localStorage. Upon interaction with the component, updates trigger the modification of both the localStorage and the component's state, ensuring consistency across the application.

One notable aspect of useLocalStorage is its seamless synchronization across different browser tabs. By dispatching storage events, it notifies other tabs of data modifications, enabling real-time updates and a cohesive user experience.

Moreover, the hook optimizes performance through the utilization of useEffect and useCallback hooks. This ensures efficient subscription to storage changes and memoization of functions, respectively, resulting in a clean and efficient codebase.

In summary, useLocalStorage simplifies the integration of localStorage functionality into React components, offering developers a robust solution for managing persistent data across sessions while maintaining optimal performance and synchronization across browser tabs.

Usage Cases

  • Managing User Preferences: Store user preferences such as app theme, preferred language, and style preferences.
  • Saving Form Data and Component State: Persist form state, filters, and other user data between sessions.
  • Controlling Interface Preferences: Store user preferences regarding styles, color schemes, and interface appearance.
  • Synchronizing State Across Browser Tabs: Ensure data and state synchronization between different tabs of the same application.
  • Managing Editable Data: Save changes made to editable data such as notes, tasks, or text documents.
  • Supporting Local Caching: Cache data to improve performance and provide quick access to frequently used information.

Creation

Code implementation
1type InitialValue<T> = T | (() => T);
2
3const useLocalStorage = <T>(
4 key: string,
5 initialValue: InitialValue<T>
6): [T, (value: T) => void, () => void] => {
7 // Check if there is a value in `localStorage` for the given `key`
8 const [value, setValue] = useState(() => {
9 try {
10 const storedValue = localStorage.getItem(key);
11
12 return storedValue ? JSON.parse(storedValue) : initialValue;
13 } catch (error) {
14 console.error('Error retrieving value from `localStorage`:', error);
15
16 // If parsing fails, return the initial value
17 return initialValue;
18 }
19 });
20
21 // Function to set value in `localStorage`,
22 // `useCallback` is used to memoize the function to prevent
23 // recreation of the function instance when `key` doesn't change
24 const setLocalStorageValue = useCallback(
25 (newValue: T) => {
26 try {
27 setValue(newValue);
28
29 localStorage.setItem(key, JSON.stringify(newValue));
30
31 // Dispatch a `storage` event to notify other tabs
32 window.dispatchEvent(
33 new StorageEvent('storage', {
34 key,
35 newValue: JSON.stringify(newValue),
36 })
37 );
38 } catch (error) {
39 console.error('Error saving value to `localStorage`:', error);
40 }
41 },
42 [key]
43 );
44
45 // Function to remove value from `localStorage`,
46 // `useCallback` is used for the same reason as above
47 const removeLocalStorageValue = useCallback(() => {
48 setValue(initialValue);
49
50 localStorage.removeItem(key);
51 }, [initialValue, key]);
52
53 // `useEffect` is used to subscribe to changes in the `localStorage` made by other tabs,
54 // and it depends on the `key` to listen for changes relevant to this specific key.
55 // If the `key` changes, `useEffect` will run again to update the subscription for the new key
56 useEffect(() => {
57 const updateAllTabs = (event: StorageEvent) => {
58 // Check if the changed `event.key` matches the current `key` and `newValue` is not `null`
59 if (event.key === key && event.newValue !== null) {
60 // Update the state with the new value from `localStorage`
61 setValue(JSON.parse(event.newValue));
62 }
63 };
64
65 // Add event listener for `storage` events
66 window.addEventListener('storage', updateAllTabs);
67
68 // Cleanup function to remove event listener when component unmounts
69 return () => {
70 window.removeEventListener('storage', updateAllTabs);
71 };
72 }, [key]); // `useEffect` depends on the `key` to subscribe to changes relevant to this `key`
73
74 return [value, setLocalStorageValue, removeLocalStorageValue]; // Re-run `useEffect` only when any of these values change
75};
76
77export default useLocalStorage;
78

Reference

Code implementation
1const [value, setLocalStorageValue, removeLocalStorageValue] = useLocalStorage(
2 key,
3 initialValue
4);
5

Parameters

NameTypeDescription
keystringThe key under which the value is stored in `localStorage`.
initialValueT | (() => T)The initial value to be stored in `localStorage` if no value is present.

Return Values

NameTypeDescription
valueTThe value retrieved from `localStorage`, or the initial value.
setLocalStorageValue(value: T) => voidFunction to set the value in `localStorage` and update the state.
removeLocalStorageValue() => voidFunction to remove the value from `localStorage` and update the state.

Example Usages

Managing Notes

This example demonstrates how to use the useLocalStorage custom hook for managing persistent state with localStorage to create a simple notes management app. The app allows users to add and delete notes while persisting changes locally and ensuring synchronization across different browser tabs, providing a cohesive experience across sessions. Notes can be removed by clicking on them, and their count dynamically updates in real time. This approach showcases efficient state management and user interaction with a straightforward and functional design.

Code implementation
1const noteMessage = 'Note with Id';
2
3const maxNotes = 6;
4
5const initialItems = loadData(3);
6
7const Container: FC = () => {
8 // Loading notes from `localStorage` using the custom hook
9 const [notes, setNotes] = useLocalStorage<IDataItems>(
10 'notes',
11 initialItems
12 );
13
14 // Handler for adding a new note
15 const handleAddNote = () => {
16 if (notes.length === maxNotes) return;
17
18 const newId = notes.length === 0 ? 1 : notes[notes.length - 1].id + 1;
19
20 setNotes([
21 ...notes,
22 {
23 id: newId,
24 name: `${noteMessage} ${newId}`,
25 },
26 ]);
27 };
28
29 // Handler for deleting a note
30 const handleDeleteNote = (item: IDataItem) => {
31 // Creating a new array of notes excluding the one to be deleted
32 setNotes(notes.filter((note) => note.id !== item.id));
33 };
34
35 return (
36 <Layer title="Container">
37 <Text>
38 Notes remain: {maxNotes - notes.length} / {maxNotes}
39 </Text>
40 {notes.length !== 0 && (
41 <List
42 items={notes}
43 onClick={handleDeleteNote}
44 />
45 )}
46 <div className="flex w-full justify-center">
47 {/* Button for adding a new note */}
48 <Button
49 onClick={handleAddNote}
50 disabled={maxNotes === notes.length}
51 >
52 Add Note
53 </Button>
54 </div>
55 <Message>Click on the note to remove it.</Message>
56 </Layer>
57 );
58};
59
An interactive output of the code above

Dynamic Styling and Preferences Management

This example demonstrates a dynamic settings interface using the useLocalStorage custom hook to manage and persist user preferences. Users can select their preferred font size, background color, and text color through an interactive UI. These preferences are applied in real-time to a preview container, allowing users to see the changes immediately. Additionally, the chosen settings are saved in localStorage, ensuring they persist across sessions and even when the tab is closed.

This approach avoids the need for props drilling, maintaining a clear separation of concerns and ensuring scalability. While the preview happens in a dedicated component in this example, the persistence logic allows the chosen settings to be applied in any part of the web application, even in the most distant components.

Code implementation
1const Settings: FC = () => {
2 // State for managing font size
3 const [fontSize, setFontSize] = useLocalStorage<number>(
4 'fontSize',
5 defaultPreferences.fontSize
6 );
7
8 // State for managing selected background color
9 const [selectedBgColor, setSelectedBgColor] = useLocalStorage<string>(
10 'selectedBgColor',
11 defaultPreferences.bgColor
12 );
13
14 // State for managing selected text color
15 const [selectedTextColor, setSelectedTextColor] = useLocalStorage<string>(
16 'selectedColor',
17 defaultPreferences.textColor
18 );
19
20 return (
21 <div className="flex flex-col gap-6">
22 <div className="flex flex-col gap-2">
23 <Label>Select Font Color</Label>
24 <Counter
25 min={10}
26 step={2}
27 max={20}
28 value={fontSize}
29 onChange={setFontSize}
30 />
31 </div>
32 <div className="flex flex-col gap-2">
33 <Label>Select Background Color</Label>
34 <div className="flex gap-2">
35 {bgColors.map((bgColor) => (
36 <ColorOption
37 key={bgColor}
38 color={bgColor}
39 selectedColor={selectedBgColor}
40 onColorChange={setSelectedBgColor}
41 />
42 ))}
43 </div>
44 </div>
45 <div className="flex flex-col gap-2">
46 <Label>Select Text Color</Label>
47 <div className="flex flex-wrap gap-2 gap-y-4">
48 {textColors.map((textColor) => (
49 <ColorOption
50 key={textColor}
51 color={textColor}
52 selectedColor={selectedTextColor}
53 onColorChange={setSelectedTextColor}
54 />
55 ))}
56 </div>
57 </div>
58 </div>
59 );
60};
61
62const Container: FC = () => {
63 // Retrieve font size and color preferences from `localStorage`
64
65 const [fontSize] = useLocalStorage<number>(
66 'fontSize',
67 defaultPreferences.fontSize
68 );
69
70 const [selectedBgColor] = useLocalStorage<string>(
71 'selectedBgColor',
72 defaultPreferences.bgColor
73 );
74
75 const [selectedTextColor] = useLocalStorage<string>(
76 'selectedColor',
77 defaultPreferences.textColor
78 );
79
80 // Apply font size and text color styles dynamically
81 const style = {
82 fontSize: fontSize,
83 color: selectedTextColor,
84 };
85
86 return (
87 <Layer title="Container">
88 <Settings />
89 <div
90 style={style}
91 className={cn(
92 'rounded-lg p-4',
93 selectedBgColor === BackgroundColors.Dark
94 ? 'bg-gray-500'
95 : 'bg-white'
96 )}
97 >
98 <span>
99 My font size and colors depend on your choice, and it will
100 also be saved even if you close the tab.
101 </span>
102 </div>
103 </Layer>
104 );
105};
106
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.