"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.
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:
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.
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:
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.
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.