Photo by Rich Tervet on Unsplash

Storing and testing state in localStorage with React

Storing state in is a great way of improving UX, so that a user can pick up where they left off when returning to an application. With React hooks, it’s super easy to abstract this functionality so it can be reused anywhere. In this article, I am going to go over the implementation, and also show you how it can be tested.

We are going to be building a good old counter app, and storing the variable in so the value is persisted between sessions.

TL;DR — you can find the final code for the project here.

Simple counter app

Let’s start by getting a simple counter app working. We have the following code:

We have a variable stored in state, and a decrement and increment button which decreases or increases the number. Now for the fun part!

Reading from localStorage

The API exposes four methods: , , and . We will want to use when we initialise the component to load in any previously persisted data, and to update the when the variable changes.

To do this, we can take advantage of ‘lazy initialisation’. All this means is that we can pass a function to which returns the initial state value, rather than passing the initial value straight in. This means we can check if there is anything stored in and initialise it to that value; otherwise initialise it to the default value.

For our code, this will look something like this:

So, when is initialised, it checks to see if contains any items with the key ‘count’. If so, the state is initialised to that value — otherwise, the initial state is set to 0.

Writing to localStorage

At this point, we are reading the state from but we never set it. You can click the buttons and increase or decrease the count, but when you refresh the page, it will get set back to the default value of 0.

To address this, we need to call when the value changes. There are a couple of ways of doing this — for example, you could add to the functions on the buttons:

However, in a real-world application where the state might be being updated from lots of places, this can quickly spaghetti and get out of hand. A simpler solution is to use to update every time the variable changes:

The has as a dependency — this means every time the count changes, will run and update with the new value.

At this stage, we have a fully functioning app which stores the count in and persists it between sessions. This is great! However, with custom React hooks, we can abstract this code to make it reusable, rather than being tightly coupled to the counter functionality. This also makes the code more testable . Decoupling logic like this from our components means we can test code in isolation rather than having the logic closely linked to the counter logic. More on that later!

Creating a custom React hook

We can abstract the state logic into its own hook:

We have pretty much the same code that we had in the counter component, but we have made it more generic so that it can be reused in any context. It takes the and as arguments, and listens for changes to the inside the to update using .

To use it inside our counter component:

This code is a lot neater than what we had before, and it’s now super easy to replace a traditional hook with a hook — you just need to pass the local storage key as the second argument, and the state will be persisted between sessions.

Testing

We can use react-hooks-testing-library to test our implementation of :

Let’s break this down. Our hook needs to do three things:

  1. it needs to initialise the state with the default argument that gets passed in;
  2. if there is already a value stored in , it should initialise the state to that value;
  3. when we update the state, it should update the value stored in .

Initialising the state with the default argument

The first test, “should set localStorage with default value”, deals with requirement one. We use react-hooks-testing-library’s to initialise our hook with a test value and a test key, and then check that the value stored in matches what we passed in.

Initialising the state to the value in localStorage

The second test, “should set the default value from localStorage if it exists”, checks that the second requirement is met. We set the to the test value before rendering our hook. We then initialise our state with an empty object, pulling the from react-hooks-testing-library’s method. Finally, we deconstruct the state from to check that it matches the state rather than an empty object, and check that itself hasn’t been updated.

Updating the localStorage state

The last test, “should update localStorage when state changes”, deals with the third requirement. We initialise our hook, and this time pull the function from so that we can update the state. We then check that has been updated with the new state value.

We can also write some tests to check that the counter integrates with properly:

Here, we set the count to an initial value of 15, then render our component and click on the increment button. We can then check that the count itself has increased, and also that the value stored in matches.

NB — if you are using an older version of jest/jsdom, you will need to mock to get it to work when you run your tests. In your file, you can add the following:

This creates a barebones implementation of so we can run the tests above and check that everything is working as expected.

To sum up, using is a great way of improving user experience by storing state between sessions, and by using custom React hooks, you can do it in a reusable, maintainable and testable way. I hope you find it useful, and if you have any questions, leave a comment below!

I am a UI developer writing about JavaScript, React and tech in general. Sign up to my newsletter on my website: https://jacktaylor.co/

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store