Custom React Hooks: useLocalStorage

In the last episode of the Custom React Hooks series, we’ve implemented the useArray hook to simplify arrays management. In today’s episode, we’ll create a hook to simplify the local storage management: useLocalStorage.

Motivation
Implementation
Usag…


This content originally appeared on DEV Community and was authored by Ludal 🚀

In the last episode of the Custom React Hooks series, we've implemented the useArray hook to simplify arrays management. In today's episode, we'll create a hook to simplify the local storage management: useLocalStorage.

Motivation

In the first place, let's see why would you need to implement this hook. Imagine that you're building an application that uses some config for each user (theme, language, settings...). To save the config, you'll use an object that could look like this:

const config = {
    theme: 'dark',
    lang: 'fr',
    settings: {
        pushNotifications: true
    }
}

Now, in the root component or in the settings page, you would let the user customize its settings, in which case you will need to synchronize the UI state with the local storage. For instance, the settings page could look like this:

Settings page preview

And the corresponding source code could be similar to this:

const defaultConfig = {
    theme: 'dark',
    lang: 'fr',
    settings: {
        pushNotifications: true
    }
};

const Settings = () => {
    const [config, setConfig] = useState(() => {
        const saved = localStorage.getItem('config');
        if (saved !== null) {
            return JSON.parse(saved);
        }
        return defaultConfig;
    });

    const handleChange = (e) => {
        setConfig(oldConfig => {
            const newConfig = {
                ...oldConfig,
                settings: {
                    ...oldConfig.settings,
                    pushNotifications: e.target.checked
                }
            };

            localStorage.setItem('config', JSON.stringify(newConfig));
            return newConfig;
        })
    }

    return (
        <>
            <h1>Settings</h1>
            <label htmlFor="pushNotifications">
                Push Notifications
            </label>
            <input
                type="checkbox"
                id="pushNotifications"
                checked={config.settings.pushNotifications}
                onChange={handleChange}
            />
        </>
    );
};

But as you can see... that's already a lot of code for just toggling a push notifications setting! Also, we have to manually synchronize the state of the configuration with the local storage, which is very cumbersome. If we don't pay enough attention, this could lead to some desynchronization.

With our userLocalStorage hook, we'll be able to abstract some generic logic in a separate function to reduce the amount of code needed for such a simple feature. Also, we won't have to synchronize anything anymore, as this will become the hook's job.

Implementation

In the first place, let's discuss about the hook's signature (which means, what are its parameters and its return value). The local storage works by associating some string values to some keys.

// Get the value associated with the 'config' key
const rawConfig = localStorage.getItem('config');

// Parse the plain object corresponding to the string
const config = JSON.parse(rawConfig);

// Save the config
localStorage.setItem('config', JSON.stringify(config));

So our hook signature could look like this:

const [config, setConfig] = useLocalStorage('config');

The hook will set our config variable to whatever value it finds in the local storage for the "config" key. But what if it doesn't find anything? In that case, the config variable would be set to null. We would like to set a default value (in our example, set a default config) for this variable in case the local storage is empty for that key. To do so, we'll slightly change the hook's signature to accept a new optional argument: the default value.

const [config, setConfig] = useLocalStorage('config', defaultConfig);

We're now ready to start implementing the hook. First, we'll read the local storage value corresponding to our key parameter. If it doesn't exist, we'll return the default value.

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

Great! We've made the first step of the implementation. Now, what happens if the JSON.parse method throws an error? We didn't handle this case yet. Let's fix that by returning the default value once more.

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

That's better! Now, what's next? Well, we just need to listen for the value changes and update the local storage accordingly. We'll use the useEffect hook to do so.

const useLocalStorage = (key, defaultValue = null) => {
    const [value, setValue] = useState(...);

    useEffect(() => {
        const rawValue = JSON.stringify(value);
        localStorage.setItem(key, rawValue);
    }, [value]);
};

⚠️ Be aware that the JSON.stringify method can also throw errors. However, this time, it is not the job of this hook to handle those errors — except if you want to catch them in order to throw a custom one.

So, are we done? Not yet. First, we didn't return anything. Accordingly to the hook's signature, we just have to return the value and its setter.

const useLocalStorage = (key, defaultValue = null) => {
    const [value, setValue] = useState(...);

    useEffect(...);

    return [value, setValue];
};

But we also to have to listen for the key changes! Indeed, the value provided as an argument in our example was a constant ('config'), but this might not always be the case: it could be a value resulting from a useState call. Let's also fix that.

const useLocalStorage = (key, defaultValue = null) => {
    const [value, setValue] = useState(...);

    useEffect(() => {
        const rawValue = JSON.stringify(value);
        localStorage.setItem(key, rawValue);
    }, [key, value]);

    return [value, setValue];
};

Are we done now? Well, yes... and no. Why not? Because you can customize this hook the way you want! For instance, if you need to deal with the session storage instead, just change the localStorage calls to sessionStorage ones. We could also imagine other scenarios, like adding a clear function to clear the local storage value associated to the given key. In short, the possibilities are endless, and I give you some enhancement ideas in a following section.

Usage

Back to our settings page example. We can now simplify the code that we had by using our brand new hook. Thanks to it, we don't have to synchronize anything anymore. Here's how the code will now look like:

const defaultConfig = {
  theme: "light",
  lang: "fr",
  settings: {
    pushNotifications: true
  }
};

const Settings = () => {
  const [config, setConfig] = useLocalStorage("config", defaultConfig);

  const handleChange = (e) => {
    // Still a bit tricky, but we don't really have any other choice
    setConfig(oldConfig => ({
      ...oldConfig,
      settings: {
        ...oldConfig.settings,
        pushNotifications: e.target.checked
      }
    }));
  };

  return (
    <>
      <h1>Settings</h1>

      <label htmlFor="pushNotifications">Push Notifications</label>
      <input
        type="checkbox"
        id="pushNotifications"
        checked={config.settings.pushNotifications}
        onChange={handleChange}
      />
    </>
  );
};

Improvement Ideas

  • Handle exceptions of the JSON.stringify method if you need to
  • If the value becomes null, clear the local storage key (with localStorage.removeItem)
  • If the key changes, remove the value associated with the old key to avoid using storage space unnecessarily

Conclusion

I hope this hook will be useful to you for your projects. If you have any questions, feel free to ask them in the comments section.

Thanks for reading me, and see you next time for a new custom hook. 🤗

Source code available on CodeSandbox.

Support Me

If you wish to support me, you can buy me a coffee with the following link (I will then probably turn that coffee into a new custom hook... ☕)

Buy Me A Coffee


This content originally appeared on DEV Community and was authored by Ludal 🚀


Print Share Comment Cite Upload Translate Updates
APA

Ludal 🚀 | Sciencx (2021-11-07T18:46:27+00:00) Custom React Hooks: useLocalStorage. Retrieved from https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/

MLA
" » Custom React Hooks: useLocalStorage." Ludal 🚀 | Sciencx - Sunday November 7, 2021, https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/
HARVARD
Ludal 🚀 | Sciencx Sunday November 7, 2021 » Custom React Hooks: useLocalStorage., viewed ,<https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/>
VANCOUVER
Ludal 🚀 | Sciencx - » Custom React Hooks: useLocalStorage. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/
CHICAGO
" » Custom React Hooks: useLocalStorage." Ludal 🚀 | Sciencx - Accessed . https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/
IEEE
" » Custom React Hooks: useLocalStorage." Ludal 🚀 | Sciencx [Online]. Available: https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/. [Accessed: ]
rf:citation
» Custom React Hooks: useLocalStorage | Ludal 🚀 | Sciencx | https://www.scien.cx/2021/11/07/custom-react-hooks-uselocalstorage/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.