Skip to main content

A Custom Hook for Local Storage: `useLocalStorage` (Part 2) - Handling Objects and Arrays #87

📖 Introduction

Following our creation of a basic useLocalStorage hook, this article explores how to make it more robust. We will learn how to handle objects and arrays, and how to synchronize state between different tabs.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • All concepts from Part 1 of this series.
  • The storage event.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Problem: Understanding the limitations of our basic useLocalStorage hook when it comes to complex data and multi-tab applications.
  • Core Implementation: How to handle objects and arrays in local storage.
  • Advanced Techniques: How to synchronize state between different tabs using the storage event.

🧠 Section 1: The Core Concepts of an Advanced useLocalStorage Hook

Our basic useLocalStorage hook works well for simple values like strings and numbers, but what about objects and arrays? localStorage can only store strings, so we need to use JSON.stringify() to store complex data and JSON.parse() to retrieve it. Our current hook already does this, so it can handle objects and arrays.

A bigger challenge is synchronizing state between different tabs. If a user has our application open in two tabs and changes a value in one, the other tab will not be aware of the change. We can solve this by listening to the storage event, which is fired whenever a change is made to localStorage from another tab.


💻 Section 2: Deep Dive - Implementation and Walkthrough

Let's enhance our useLocalStorage hook to handle state synchronization.

2.1 - The Enhanced useLocalStorage Hook

// useLocalStorage.js
import { useState, useEffect, useCallback } from 'react';

function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key);
if (saved !== null) {
return JSON.parse(saved);
}
return defaultValue;
} catch {
return defaultValue;
}
});

useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key) {
setValue(JSON.parse(e.newValue));
}
};

window.addEventListener('storage', handleStorageChange);

return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [key]);

const setStoredValue = useCallback((newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}, [key]);

return [value, setStoredValue];
}

export default useLocalStorage;

Step-by-Step Code Breakdown:

  1. useState with a try-catch block: We've made our initial state retrieval more robust by wrapping it in a try-catch block.
  2. useEffect for the storage event: We use a new useEffect hook to add an event listener for the storage event.
    • When the event fires, we check if the key that changed is the same as the key our hook is managing.
    • If it is, we update our state with the new value from the event.
    • The cleanup function removes the event listener when the component unmounts.
  3. useCallback for setStoredValue: We wrap our setValue function in useCallback for performance optimization.

🛠️ Section 3: Project-Based Example: A Synchronized Theme Switcher

Now, let's use our enhanced useLocalStorage hook to create a theme switcher that syncs between tabs.

// ThemeSwitcher.js
import React from 'react';
import useLocalStorage from './useLocalStorage';

function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorage('my-app-theme', 'light');

const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};

return (
<div className={`app ${theme}`}>
<h1>Current Theme: {theme}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}

export default ThemeSwitcher;

Now, if you open this component in two different tabs and click the "Toggle Theme" button in one, you will see the theme change in the other tab as well.


💡 Conclusion & Key Takeaways

In this article, we've made our useLocalStorage hook much more powerful. It can now handle complex data types and synchronize state between different tabs, making it a truly robust solution for managing persistent state in React applications.

Let's summarize the key takeaways:

  • JSON.stringify() and JSON.parse() are essential for storing and retrieving complex data in localStorage.
  • The storage event allows us to synchronize state between different tabs.
  • By creating a robust useLocalStorage hook, we can simplify state management and improve the user experience in our applications.

Challenge Yourself: To solidify your understanding, try to build a simple to-do list application that uses the useLocalStorage hook to persist the list of to-dos and syncs them between tabs.


➡️ Next Steps

You now have a solid understanding of how to create a robust and reusable useLocalStorage hook. In the next article, "Rules of Hooks and Best Practices", we will review the rules of hooks and discuss some best practices for writing your own custom hooks.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • storage event: A browser event that is fired on a window when a storage area (localStorage or sessionStorage) has been changed in the context of another document.
  • State Synchronization: The process of keeping the state of multiple components or applications consistent.

Further Reading