Photo by Lautaro Andreani on Unsplash
Day 41- Persisting state in Local storage
Hi and welcome to day 41 of my 100 days of code challenge! πͺ
Table of contents
Initializing state with a callback (lazy initial state)
I am going to implement a brand new feature for our app, and this is the ability to store your watch list right on your device using local storage which involves persisting the watch list in local storage. Let's break it down into two steps.ππ
First, within the
App
component, whenever there's an update to the watch list state, we'll sync it with local storage, storing the latest data there.Then, upon each app load, specifically when the
App
component first mounts, we'll retrieve the stored data from local storage and update the watched state accordingly.
Let's kick things off with the first part, where we'll store the watched movies into local storage every time they are updated.π
And we can actually do that in two different places.π€
The first option is to store the data into local storage each time a new movie is added, right within the event handler function responsible for adding movies to the watch list. This way, every time a new movie is added, the updated watch list can be stored in local storage.
The second option is to handle it within an effect. Let's implement both approaches, starting with the event handler function
If you're not familiar with local storage, it's essentially a key-value pair storage mechanism accessible in the browser. It allows us to store data for each domain. Essentially, the data we store in local storage will only be accessible via the specific URL where it's stored, not on other URLs.
To use local storage, we simply call the localStorage
function, which is available in all browsers. Then, we use the setItem
method, passing in the key name (the identifier for the data we want to store) as the first parameter, and the actual data as the second parameter.ππ
Now hereππ, we cannot simply use the watched array like this because it has just been updated here.
Since this updating process occurs asynchronously, at this point, the state is still stale, meaning it's the old version before a new movie has been added. Thus, we need to create a new array based on the current state plus the new movie.
Finally, we need to convert all of this into a string because in local storage, we can only store key-value pairs where the value is a string. Let's utilize the built-in JSON.stringify
method for this conversion.ππ
Now, let's add two movies to the list. However, if we reload the page now, they won't magically reappear in the UI.π
But we can still check if they have been stored in local storage. To do this, go to the DevTools again, navigate to "Application," and click on "Local Storage." There, you'll see the URL listed.ππ
And indeed, here βοΈβοΈ we can see that our array of watched movies has indeed been stored in local storage.
As I was saying, we can also handle it directly inside an effect. Instead of implementing it within the event handler function, we'll do it in an effect.
Let's create a new effect function, and as we discussed, we want this effect to run every time the watched movies are updated. So, we can easily achieve this by just referencing the watched movies state within the dependency array of the effect.π
Now, we don't need to create any new array because this effect will only run after the movies have already been updated, meaning the watched
state reflects the new state. So, we can simply use that here. We can observe that our local storage has already been updated because this effect ran when the component was first mounted.π
At this point, the watched state is by default still an empty array. Therefore, React sets that here to the empty array.π
So, as we add new movies to the list now.π
We should see our local storage being updated.π
Now, we need to take care of the second part, which is to read this data back into the application as soon as the App
component mounts. So the component that owns this watched state.β
Now how can we do that?
Well, one might initially think that we should use another effect to fetch the data from local storage on the initial render, and then store that data in the watched state. However, there's actually a better way to handle this.
Let's deactivate this watched state here and maybe move it down here, then duplicate it because we still need this state, of course.ππ
What we're going to do now is, instead of just passing in a value, we'll pass in a callback function. This is because the useState hook also accepts a callback function instead of just a single value. By doing this, we can initialize the state with whatever value this callback function will return.
We'll create a brand new function, a new variable called storedValue
, and then we can simply read from local storage using the getItem
method. So, it's getItem
followed by the key that we used before to store the data in local storage, which is watched
. Then, we just need to return this value.ππ
Again, React will call this function on the initial render, and we'll use whatever value is returned from this function as the initial value of the state. It's important to note that this function needs to be a pure function and cannot receive any arguments. Therefore, passing arguments here βοΈwon't work.
Exactly, just a very simple pure function that returns something, and that something will be used by React as the initial state.
And also just like the values that we pass in, React will only consider this function here βοΈon the initial render.
So this function is only executed once on the initial render and is simply ignored on subsequent re-renders.
It seems there is a problem here, so let's check that out. The error message indicates that watched.map
is not a function, and this is because currently, the data in local storage is a string.π
Right, remember that we stored the data as a string by using JSON.stringify
, so when we retrieve the data, we need to convert it back to its original format by using JSON.parse
.ππ
We've successfully stored the data in local storage and retrieved it as the application loads. Whenever the initial value of the state depends on some sort of computation, it's best practice to pass in a function like thisβa function that React can execute on its initial render.
So, we should not call a function inside useState directly, but instead pass in a function that React can then call later.
when we delete a movie ,π it actually automatically got removed from the local storage as well. Why is that? Well, it's because thanks to our effect here, we have effectively synchronized the watched state with our local storage. So when the watched state changes, our local storage changes as well.π€β
So, before deleting this watched movieπ
So, if you delete the movie from the UI, it's also deleted from local storage. Thank you for reading!
To be continued βπββοΈ.