Redux: Persisting the State to the Local Storage

Dan Abramov
InstructorDan Abramov
Share this video with your friends

Social Share Links

Send Tweet

We will learn how to use store.subscribe() to efficiently persist some of the app’s state to localStorage and restore it after a refresh.

[00:00] I want to persist the state of the application in the localStorage using browser localStorage API. I'm going to write a function called load state and a new model that I'm going to call localStorage AS that is going to use the localStorage browser API.

[00:19] The load state function is going to look into local storage by key, retrieve a string, and try to parse it as JSON. It's important that we wrap this code into try/catch because calls to your localStorage getItem can fail if the user privacy mode does not allow the use of localStorage.

[00:39] If the serialized state is no it means that the SKI does exist so I'll return undefined to let the reducers initialize the state instead. However if the serialized state string exists I'm going to use JSON.parse in order to turn it into the state object. Finally in case of any errors I'm going to play it safe and return undefined to let reducers initialize the application.

[01:08] Now that I have wrote the function to load the state, I might as well write a function that saves the state to localStorage. It's going to accept the state as an argument, and it's going to do the exact opposite thing.

[01:22] First it serializes it to string by using JSON.stringify. This will only work if the state is serializable, but this is the general recommendation in Redux. Your state should be serializable. Now that both JSON.stringify and localStorage set items goal can fail so it's important that we catch any errors to prevent the app from crashing.

[01:46] I'm just going to ignore them, but I might as well log them somewhere. Now I am also importing the save state fiction I just wrote in the index.js. I need to save the state any time the store state changes, so I'm using the store subscribe method to add a listener that will be invoked on any state change, and I'm passing the current state of the store to my save state function.

[02:13] Let's see if it worked. I'm adding a few todos. I'm toggling one of the todos, and then I'm refreshing. The state of the app is preserved across reloads, and in fact the visibility filter is also preserved, which is probably not what we want, because usually we want to persist just the data and not the UI state.

[02:36] To fix this rather than pass the whole state object I'll just pass an object with the todos field from the state object. This way if I start with a clean localStorage and I add a couple of todos, and then I toggle one of them and change the visibility filter, after refresh the todo's are still there, but the visibility filter gets reset to all.

[03:00] When the store is created the todo's are preserved from the persisted state, but the visibility filter is initialized by the reducer. However, the current code has a bug. If I add a new todo to the existing todo' it's not going to appear, and react is going to log a warning saying that I encountered two children with the same key, zero.

[03:23] What this means is that in the todo list component when we render the todos, we use the todos ID as a key. The todo ID is assigned in the at todo action creator, and it uses a local variable called next todo ID as a counter.

[03:41] It's supposed to be unique. However if the application runs the second time the next todo ID is going to be initialized to zero again so the new todo which is added also has an ID of zero just like the very first todo.

[03:58] To avoid problems like this I'm going to install an MPM module called node-uuid. It is a very tiny module, and it exports a couple of functions. The function we're going to use is called V4, which is just a name of the standard.

[04:15] It generates a unique string ID every time, and we're going to use this ID instead of a counter. I'm replacing the counter decoration with an import of V4 from the node-uuid, and I'm calling V4 to generate a unique ID in my action creator.

[04:34] Now let's run the app again with a clean localStorage. I'm adding a couple new todos, I'm toggling them, and these arrive refreshed in the app. I can change the visibility filter, but it doesn't get persisted, because we only want to persist the data.

[04:50] Finally I'm adding a new todo, and it gets added successfully. It also gets persisted so I can refresh, do something with it, refresh, and it's there again in the correct state.

[05:04] There is just one more thing left to do. We're currently call save state inside the subscribe listener so it is called every time the storage state changes. However we would like to avoid calling it too often because it uses the expansive stringify operation.

[05:23] To solve this I'm going to use a library called load dash which includes a handy utility called throttle. Wrapping my call back in a throttle call insures that the inner function that I pass is not going to be called more often than the number of milliseconds I specify.

[05:43] Now even if this store gets abated really fast we have a guarantee that we only write to the localStorage at most once a second. Now I'm adding the import for throttle from load dash and know that I imported directly from a file called Throttle so that we don't end up with a whole load dash in our bundle just because of a single function.

[06:06] Let's recap how we added the localStorage persistence to the tutor's app. First we created the new module with load state and save state functions. Load state looks into the localStorage, and if there is a serialized string of our state it tries to parse it as JSON.

[06:27] If something goes wrong we return it undefined so that the app doesn't crash. We use the return value of load state as the second argument to create store so that it overrides the initial state specified by the reducers.

[06:43] We want to be notified of the changes to the store state so we subscribe to it. We wrap our subscriber in throttle function from load dash in order to insure that it doesn't get called more often than once a second.

[06:58] We only want to keep the todos and not the visibility filter, so we explicitly parse an object that contains just the todos from the current state. Instead save state we're going to serialize the current state to string with JSON.stringify and try to set item, but if something fails, we're just going to ignore this error so that the app doesn't crash.

[07:24] Finally we change the ID generation for todos from a simple in memory counter to a function from node.uuid that generates a unique ID string every time.