React Custom Hooks #1: Manage Your Data with useLocalStorage

Manage your data efficiently with the useLocalStorage custom hook. Learn how to simplify data storage and retrieval in your React apps with this powerful tool.

#react
#javascript
#hooks
React Custom Hooks #1: Manage Your Data with useLocalStorage
Picture by Marco

After I started creating my design system mountain-ui, I thought what I’ve learned so far in my career and what I’ve been applying in its implementation could be of good use for other developers who’d like to learn better how to exploit the React API at its full potential.

So, I’m starting a new series of posts where we will see together how I created some custom hooks which I use day by day. Sometimes I adapted existing solutions, others I created them from scratch based on my needs.

The reasons for a useLocalStorage hook

More than once, I’ve found myself with the need to persist a piece of information across user sessions, such as the selected font size for the page, or the dark mode preference for a user.

Having something simple to sync the localStorage with a React state was a must-have for me.

When Hooks came in with React 16.8, many developers started getting their hands dirty with them, creating custom hooks able to interact with the browser’s API and create a better developer experience.

That’s when I found out about the first implementation of this hook, that through time has been iterated consistently to always provide better support for the developer's needs.

In mountain-ui, the library I’m building for personal development and my personal site, I created a package where I’ll define all the hooks that we’ll see in this series, feel free to use it and contribute if you’d like to.

How does it work behind the scene?

This custom hook gives developers the fundamental features to interact with the localStorage:

  • Store a key/value pair in the localStorage of the client device.
  • Sync the stored value with a React state to rerender the component when a new value is set.
  • It also takes care of SSR, avoiding errors when the hook is invoked at build time or server-side.

Coding time

Let’s now check together this hook implementation and we’ll see step by step what’s going on when we invoke the hook inside a component!

jsx
import { useEffect, useState } from 'react';
import { hasWindow } from '@mountain-ui/utils';

/**
*
* @param key string
* @param initialValue any
* @returns {UseLocalStorageResult}
*/
function useLocalStorage(key: string, initialValue?: unknown) {
/**
* The loadStoredValue retrieves the stored value
* from the localStorage if it exists.
* In case the hook is used in SSR, it returns the
* initial value until it gets hydrated in the client.
*/
const loadStoredValue = () => {
if (!hasWindow()) return initialValue;

try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(
`#useLocalStorage: an error occurred loading the localStorage key “${key}”:`,
error
);
return initialValue;
}
};

/**
* This state holds the value associated with the store key
* and is served to the component.
*/
const [storedValue, setStoredValue] = useState(loadStoredValue);

/**
* The setValue function checks whether is possible
* to store a new value for the key argument and
* update the local state to notify the component about the change
*/
const setValue = (value: unknown) => {
if (!hasWindow()) {
console.warn(
`#useLocalStorage: impossible to set the localStorage “${key}” inside a no-client context.`
);
}

try {
const valueToStore = typeof value === 'function' ? value(storedValue) : value;

window.localStorage.setItem(key, JSON.stringify(valueToStore));

setStoredValue(valueToStore);
} catch (error) {
console.warn(`#useLocalStorage: error setting the localStorage key “${key}”:`, error);
}
};

useEffect(() => {
setStoredValue(loadStoredValue());
}, []);

return [storedValue, setValue];
}

export default useLocalStorage;

function App() {
const [darkMode, setDarkMode] = useLocalStorage('dark-mode', 'light');

return (
<div>
<p>Dark mode: {darkMode}</p>
<button onClick={() => setDarkMode('light')}>Switch to light theme</p>
<button onClick={() => setFontSize('dark')}>Switch to dark theme</p>
</div>
);
}

Ok, this is quite a long piece of code. But let’s start from the beginning to understand what it does:

  • First of all, it imports a utility for the implementation, the function hasWindow that you can find in the mountain-ui repository.
  • After the initial imports, we start declaring the hook function. It expects two arguments, the first is mandatory and is the key used to store a value in the localStorage. The second is optional and is an initialValue to save associated with the key argument.
  • The first step of the implementation is to define a function loadStoredValue that allows retrieving from the localStorage the value saved with the key argument. It also controls whether the hook is called in the server using the hasWindow function, preventing errors while accessing the window object in the following lines.
  • Once we have defined the loadStoredValue init function, we create a new React state to hold the value stored. This state is essential to keep synced with the localStorage value and the UI so that the components using this hook can rerender once a new value is stored.
  • Now that a state has been created, the next step is to define an updater function able to store the new value in the localStorage and update the state previously created.
  • Finally, before returning the result of the hook call as [storedValue, setValue], we apply a side effect that should run only on the mount to restore the value from the localStorage and sync it with the React state.

Hook usage

As I pointed out before, there are tons of usages for this custom hook, it just depends on your needs. You may use this hook to create other custom implementations and extract more logic into reusable hooks!

Conclusion

We are at the end of this first React Hooks story, I hope it could be helpful and I’d like to hear from you for any doubt!

You can also find more about the resources I’m using on the following pages:

Thanks for taking the time to read it, see you with the next post! 🚀

Last updated: