React šŸ’˜ localStorage: persisting state with a custom hook

It doesnā€™t have to be hard

Damiano Magrini
Level Up Coding

--

Photo by S Migaj on Unsplash

While building a React application, you might come across some cases (say, a dark mode toggle) when it can be handy to automatically persist a componentā€™s state across page reloads ā€” out of the many solutions that could be employed, the easiest and most practical to put into practice is, without doubt, the local storage API.

An overview of local storage

The localStorage object provides access to simple, synchronous key-value storage, where both the key and the value are strings. There are two main methods weā€™ll need to keep in mind:

  • getItem, which takes in a keyName as its only argument and returns the value associated with that key (or null if no value exists at that key), and
  • setItem, which takes in a keyName and value as its two arguments and, unsurprisingly, stores the specified value at the given key.

Other methods worth mentioning, but not relevant to the purposes of this article, include removeItem (deletes a value at a given keyName) and clear (takes in no arguments and deletes all stored values).

What if we want to store a value that isnā€™t a string, though? For that, weā€™ll have to convert it into a string by using the JSON.stringify method ā€” then, when we want to retrieve the data, weā€™ll have to JSON.parse it to get back the original value. Note that this allows storing strings, numbers, objects, arrays, Booleans, and null values: undefined values and functions will simply be ignored by JSON.stringify, so they cannot be stored in local storage.

Building a custom hook

Letā€™s now go over the process of creating a custom React hook to automatically persist a componentā€™s state into local storage.

We will begin by creating a usePersistedState function, which takes in two parameters:

  • defaultValue, the initial state if it cannot be retrieved from local storage, and
  • localStorageKey, the unique key used for storing and retrieving the state in local storage.

Retrieving the state from local storage

Letā€™s now write the logic to handle basic state retrieval: as we mentioned, we will take the value from local storage and JSON.parse it to get back the original value. Weā€™ll use the useState hook to store our state in memory (this is how custom hooks are built ā€” by composing native hooks such as useState).

We will now handle a couple of edge cases. Namely:

  • if no value is stored in local storage, we will use the provided default value, and
  • if invalid JSON is stored, JSON.parse will throw an error and we will, again, use the default value.

In code, this corresponds to:

We have successfully retrieved the initial value! Now, letā€™s go on to updating it in local storage whenever the state changes.

Listening to changes and updating local storage

This oneā€™s easier: whenever value changes, weā€™ll just stringify it and store it back into local storage, using the provided localStorageKey. Weā€™ll use a useEffect hook for that.

Before proceeding to the next step, a note on performance: the localStorage API, as weā€™ve said, is synchronous, and that means it blocks the main thread. Now, this isnā€™t a problem with simple data, but it may become an issue if you use it in apps with a lot of complex data. To improve performance, you have two options:

  • Instead of saving whenever value changes, just save periodically every set amount of time.
  • Or, debounce state updates: that is, whenever value changes, wait for a few moments, and only then save the state into local storage. This way, multiple, consecutive state updates will only result in a single local storage write.

Wrapping everything up: making the hook usable

You may have noticed that this whole time, our hook didnā€™t return anything: weā€™ll fix that now, by returning value and setValue, just like the useState hook does. So, putting it all together, hereā€™s what weā€™ve got:

Et voilĆ ! A simple React hook to retrieve and persist state in local storage.

Final considerations

We already saw the performance implications of this approach, so letā€™s skip to some other considerations. First, from a security standpoint, you should remember that local storage is stored unencrypted on disk and is accessible to all JavaScript code that runs on your domain ā€” this is not a problem per se, but it means you need to pay extra care to protect your website against XSS (cross-site scripting) attacks.

Secondly, it should be mentioned that there is a hard limit on the amount of data you can store in local storage: 5 megabytes ā€” one byte more than that, and localStorage.setItem will throw an error. But this shouldnā€™t be an issue as long as youā€™re not storing extremely complex data (like entire files) ā€” for that, you may want to look into other solutions, such as the IndexedDB API.

To recap: what we learned

Letā€™s consolidate our knowledge! In this article, weā€¦

  • learned the basics of the localStorage API,
  • found out about JSON parsing and serialization,
  • applied our knowledge of useState and useEffect to create a custom React hook, and
  • made some considerations about using the localStorage API.

Do you think youā€™ve mastered all these concepts? If so, I encourage you to practice on your own: for example, you can try to improve this hook by debouncing state changes. Otherwise, feel free to get in touch and ask for clarifications! Iā€™ll be there to help.

--

--

Passionate about mathematics, philosophy, UI/UX design, frontend web development, and writing ļøšŸ–Šļø Sharing is caring, knowledge too!