Custom React Hooks: useLocalStorage
Tags: react, javascript, programming, webdev
Published: 2021-11-07 18:46:27

In the last episode of the [Custom React Hooks](https://dev.to/iamludal/series/14964) series, we've implemented the [useArray](https://dev.to/iamludal/custom-react-hooks-usearray-101g) hook to simplify arrays management. In today's episode, we'll create a hook to simplify the local storage management: `useLocalStorage`.

- [Motivation](#motivation)
- [Implementation](#implementation)
- [Usage](#usage)
- [Improvement Ideas](#improvement-ideas)
- [Conclusion](#conclusion)

## 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:

```jsx
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpr40cqmb5p2ksk69ll8.png) 

And the corresponding source code could be similar to this:

```jsx
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.

```jsx
// 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:

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
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:

```jsx
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](https://codesandbox.io/s/red-grass-0pdve?file=/src/App.tsx).

{% codesandbox 0pdve %}

---

## 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](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://www.buymeacoffee.com/iamludal)