React š localStorage: persisting state with a custom hook
It doesnāt have to be hard
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 akeyName
as its only argument and returns the value associated with that key (ornull
if no value exists at that key), andsetItem
, which takes in akeyName
andvalue
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, andlocalStorageKey
, 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
anduseEffect
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.