How to use React Custom Hooks to persist React state in Local Storage.

How to use React Custom Hooks to persist React state in Local Storage.

"Another day, another dollar". If I had a dollar for the number of times I have said state in the past couple of days, I would have exactly 988,673,9823 dollars, & yes, this is an entirely arbitrary number.

Now that we have broken ice, let's talk about why you are here, reading tiny pixels of texts on your screen.

Introduction

localStorage is a part of the Web Storage API, it is used to store items of data (the geeks call it key-value pairs) in your browser. What the geeks are saying here is when you use localStorage, you must give your data (also called value) a key (which serves as an identifier for your data). Hence, key-value pair(s). It is also important to note the data stored are of type string and have no expiration date.

You're probably wondering why I have written like a medieval prince posing for a photo on a horse on the streets of Camelot, so let me break all of these down.

What all the cool stuff above means is data stored in localStorage is stored as a string & will exist throughout the lifetime of your browser, so should you close your browser, you do not lose the data stored. However, you can choose to remove or clear your data from localStorage when it has served its purpose. The localStorage API comes with a removeItem() method which removes your data from storage by key (remember the geeks & how they call it key-value pairs?) & a clear() method which clears the entirety of your data from storage.

You can find more information to better understand localStorage here & here

React Hooks & The State problem

We'll use a simple counter example to understand why we need to use localStorage to persist React State. We'll also write some code & then talk about what we learn from that bit of code.

Let's create a React app npx create-react-app local-storage-hooks

Then, delete all the boilerplate code in our App.js & replace it with the following code. So our App.js now looks something like this:

#App.js

import { useState } from 'react';
import './App.css';

const App = () => {
  const [count, setCount] = useState(0);

  const handleIncrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div className='app'>
      <div className='app__container'>
        <p>your count is: {count}</p>
        <button onClick={handleIncrementCount}>click me!</button>
      </div>
    </div>
  );
};

export default App;

Okay, we got our hands dirty but what have we just done? We imported useState from 'react' & we set our initial state value to 0 const [count, setCount] = useState(0);

We then created a function handleIncrementCount which increases the count by 1 each time the button's clicked. This works fine until our component re-renders, we lose our current state value of button clicks & default to the initial state value of 0. Let me show you.

React App - Google Chrome 2022-04-11 04-29-47.gif

To be able to keep our current state value, we have to persist our React State in drumroll, please... localStrorage. There are two ways to go about this, both would mean using the React useEffect Hook but the entire point of this article is to show how we can write a custom hook & efficiently reuse this custom hook as many times as we like as our application grows.

Let's talk about these two methods, shall we?

The Double useEffect Method

I know this isn't the smoothest title out there, if I had to rank it, it would rank the second-smoothest title but let's ignore my not-so-great naming skills & focus on what's really important. Code.

To really understand this, you should have a fairly decent idea of what the useEffect hook does & how it does it, if you would like to read more, there's a great article that explains useEffect & how to think about handling side effects in your application here. If your knowledge of useEffect isn't great, don't worry about it, I will do my best to explain as we go.

Inside our App.js, let's write some new code. The code from the previous example is in the starter branch of the github repository here.

#App.js 

import { useState, useEffect } from 'react';
import './App.css';

const App = () => {
  const [count, setCount] = useState(0);

  const handleIncrementCount = () => {
    setCount(count + 1);
  };

  const handleResetCount = () => {
    setCount(0)
  }

  useEffect(() => {
    const data = localStorage.getItem('click-count');

    setCount(JSON.parse(data));
  }, []);

  useEffect(() => {
    localStorage.setItem('click-count', JSON.stringify(count));
  });

  return (
    <div className='app'>
      <div className='app__container'>
        <p>your count is: {count}</p>
        <button onClick={handleIncrementCount}>click me!</button>
        <button onClick={handleResetCount}>reset count</button>
      </div>
    </div>
  );
};

export default App;

Whew! That's quite a lot of code but do not fret! I'll explain what we've just done. We have now improved upon our first example, by importing the useEffect hook & we have used it twice, one with a dependency array, the other without.

To use the double useEffect method, we should note that the placement of each useEffect is important because React will run the topmost one first.

Let's understand what's going on in the second useEffect , we use the setItem() method from localStorage by setting the value of click-count which is our key (key-value pairs, remember?) to our state count. We also call JSON.stringify() as we set our item because data in localStorage is stored as a string, remember? Great.

localStorage.setItem('click-count', JSON.stringify(count));

Not adding a dependency array to the second useEffect means it'll run each time our application renders, & because we want to be able to keep track of our change in state on every button click, this useEffect is perfect for us.

In the first useEffect, we declare a variable & use the getItem method to well, get our key click-count

const data = localStorage.getItem('click-count');

We then set the value of our state count to data by doing this: setCount(data) we also call JSON.parse() to convert our stored data from a type string to a type number to be able to use it the way we want.

Let's have a look at a short demo:

React App - Google Chrome 2022-04-11 05-59-15.gif

As you can see, when we refresh our page, we do not lose our count value, we have now persisted our React State inside localStorage.

Cat Break!

Whew! We have written quite a bit of code. I'm sure you need a break from me talking about data so much. I know I do.

via GIPHY

Now that we're all fistbumping & smiling, let's get right back into it.

The Custom Hook Method

Custom hooks are simply high functions, they take arguments & return values we can then use by abstraction. They are essentially functions with an attitude (but don't tell anyone I said that)

Let's create a new folder inside our src folder & call it hooks, inside the hooks folder, we'll create a file & call it useLocalStorage.js. our folder structure should look something like this:

hooks file structure.png

Once we have that set up, let's write some more code.

#useLocalStorage.js

import { useState, useEffect } from 'react';

export const useLocalStorage = (initialValue, key) => {
  const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue !== null ? JSON.parse(storedValue) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};

& inside our App.js, let's write some more code.

#App.js

import { useLocalStorage } from './hooks/useLocalStorage';
import './App.css';

const App = () => {
  const [count, setCount] = useLocalStorage(0, 'click-count');

  const handleIncrementCount = () => {
    setCount(count + 1);
  };

  const handleResetCount = () => {
    setCount(0)
  }

  return (
    <div className='app'>
      <div className='app__container'>
        <p>your count is: {count}</p>
        <button onClick={handleIncrementCount}>click me!</button>
        <button onClick={handleResetCount}>reset count</button>
      </div>
    </div>
  );
};

export default App;

What we have done here is create & export a custom hook useLocalStorage.js which accepts two arguments initialValue & key, this custom hook's state value is a function that holds & returns a variable const storedValue.

We then check if the storedValue is null, if it is not equal to null, we return a parsed value for storedValue otherwise we return the initial state value initialValue, like so:

return storedValue !== null ? JSON.parse(storedValue) : initialValue;

Our custom hook also uses a single useEffect hook that sets the value of our localStorage item when our component mounts. The key argument passed to localStorage.setItem here is used to set the key (remember, key-value pairs?). Our useEffect hooks also holds two items in the dependency array [key, value]. These items in the dependency array will determine conditions for our component re-render should they change.

Finally, our custom hook returns [value, setValue].

Inside our App.js, we import { useLocalStorage }, we then change our state array to use our custom hook, like this:

const [count, setCount] = useLocalStorage(0, 'click-count');

The major difference here is we have now passed useLocalStorage(0, 'click-count') as arguments intialValue & key respectively into our custom hook useLocalStorage(initialValue, key).

The event handlers handleIncrementCount & handleResetCount remain unchanged. When we run a demo, our result remains the same.

React App - Google Chrome 2022-04-11 07-01-53.gif

Closing Remark

I know! That was a lot to take in but in the end, we did great! Now, we have created our own custom hook to persist React state in localStorage. The best part is this is easily transferrable to creating all kinds of custom hooks to suit your development needs. It also saves you time, it scales & it is highly efficient. So write a custom hook whenever you can.