Redux: Persisting the State to the Local Storage

Dan Abramov
InstructorDan Abramov

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 3 years ago

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.

Pavel Dolecek
Pavel Dolecek
~ 6 years ago

Is it important to distinguish between null and undefined on initializing reducers?

Specifically, can I replace:

if (serializedState === null) {
  return undefined
}
return JSON.parse(serializedState)

with just

return JSON.parse(serializedState)

as

JSON.parse(null) // => null
Pavel Dolecek
Pavel Dolecek
~ 6 years ago

Lesson learned (the hard way) with node-uuid

It seems like a small library, but when used with Browserify on client, it will bundle also Crypto polyfill and this:

const uuid = require("node-uuid")
console.log(uuid.v4())

compiled with browserify test.js -o test.bundle.js will output 562KB monster

Zhentian Wan
Zhentian Wan
~ 6 years ago

https://github.com/reactjs/redux/blob/master/test/createStore.spec.js#L546

looks like second args for createStore() can accept undefined, [], {} & fn but not null.

Pavel Dolecek
Pavel Dolecek
~ 6 years ago

Yep, correct... it has to be undefined otherwise

https://github.com/reactjs/redux/blob/master/src/combineReducers.js#L111 state would not use default value {}

https://github.com/reactjs/redux/blob/master/src/combineReducers.js#L128 would throw

Dean
Dean
~ 6 years ago

I'd like to know what tradeoff/benefit is to to use store.subscribe() over putting the localStorage capability in a middleware?

Dan Abramov
Dan Abramovinstructor
~ 6 years ago

Yes, the distinction between null and undefined important. The ES6 feature we use in Redux (as noted in the previous lesson) is that default argument syntax only kicks in if state is undefined. If you pass null, the combined reducer won’t use the default {} state, and will crash trying to access previous values of the state of the nested reducers.

Dan Abramov
Dan Abramovinstructor
~ 6 years ago

It seems like a small library, but when used with Browserify on client, it will bundle also Crypto polyfill

Good catch, I didn’t realize this. While I believe this is configurable, this fork might be a better choice (uuid on npm).

Dan Abramov
Dan Abramovinstructor
~ 6 years ago

I'd like to know what tradeoff/benefit is to to use store.subscribe() over putting the localStorage capability in a middleware?

No real difference IMO.

Anshuman
Anshuman
~ 6 years ago

Regarding the use of uuid - isn't it recommended to have values for the key prop based on the identity of the item in question?

Thus - if one doesn't have server-assigned ids for each item - making a hash of a given item's data potentially a better choice for the key. Here it doesn't make sense because one could trivially generate two todos with identical content and properties, but perhaps in a table of user records a hash is a better choice?

I am struggling with finding a general answer to this question as I start to convert a large project to an SPA with state persisted/cached via local storage.

References: https://github.com/facebook/react/issues/1342#issuecomment-39230939 https://facebook.github.io/react/docs/reconciliation.html#trade-offs

Piyabhum Sornpaisarn
Piyabhum Sornpaisarn
~ 5 years ago

I am using combine reducer here and I also use reduxdev tools. I can see that my persistedData have the previous sate on it but when I tried to put into store it has error. Can you please explain ? my store look like this export const store = createStore(rootReducer, [persistedState], composeEnhancers( applyMiddleware(thunk)) )

Evan Gillogley
Evan Gillogley
~ 5 years ago

This seems very hacky and hard coded and not really a part of redux. Is there any way to use a middleware per reducer (meta reducer) and wrap it like so? - combineReducer({ todos: localstorageMeta('todos', todosReducer) }) then everything is stored without any configuration - the method above is only useful for async stores like indexDB - the meta reducer - here's a quick implementation -

export function localstorageMeta (key: string, reducer: any): any {
  return function(state: any, action: any): any {
    let nextState = reducer(state, action);

    let storageState = JSON.parse(localStorage.getItem(key));
    if (action.type === RESET_STATE || action.type.includes('DELETE')) {
      localStorage.removeItem(key);
    } else if (!state && storageState || action.type === '@@redux/INIT') {
      nextState = storageState;
    } else if (nextState && nextState !== storageState) {
      localStorage.setItem(key, JSON.stringify(nextState));
    }
    return nextState;
  };
};

// same with cookies
const Cookie = require('js-cookie');
import { RESET_STATE } from './reset';
export function cookieMeta (
  key: string,
  reducer: any,
  expiry: Date | number = 365,
  path: string = '/',
  domain: string = window.location.hostname): Function {
  return function(state: any, action: any): any {
    let nextState = reducer(state, action);
    let cookieState = Cookie.getJSON(key);

    if (action.type === RESET_STATE || action.type.includes('DELETE')) {
      Cookie.remove(key);
    } else if (!nextState && cookieState || action.type === '@@redux/INIT') {
      nextState = cookieState;
    } else if (nextState && nextState !== cookieState) {
        Cookie.set(key, nextState, { expires: expiry, path: path, domain: domain, secure: process.env.local });
    }
    return nextState;
  };
};
Omri Mor
Omri Mor
~ 5 years ago

I think the font is Operator Mono

J. Matthew
J. Matthew
~ 3 years ago

Regarding the use of uuid - isn't it recommended to have values for the key prop based on the identity of the item in question?

While I'm not certain what are considered best practices for creating IDs, I can say from personal experience that it's risky to use an object's own properties to construct a unique ID, because property values can and do change—even when you think they won't. Dates get reformatted, names include typos, etc. Even the most so-called "fixed" data is subject to revision. That might not be an issue if you're not referencing the IDs (but are only using them to prevent duplicates, for example), but it becomes a huge issue as soon as you relate things to other things based on IDs. As soon as one of those IDs changes, you've got some painful work to do updating the broken relationships on who knows how many records.

Whereas if you use something like a uuid, which is totally decoupled from what it's identifying, then you can change any other property at will without concern, and you'll never have reason to change the ID and break relationships. (The same holds true for the numeric ID Dan was using previously.) There's clearly no relational business going on in this lesson, so there's nothing stopping you from using a property-based ID, but it's probably a good idea to get in the habit of not doing that.

One scenario where a decoupled ID would present a challenge is if you didn't know the ID of your target and wanted to look it up based on the object's properties. But that's what search algorithms are for, and you can also do something simple like constructing a lookup at runtime that maps a property-based hash to a uuid or whatever. Then you get the best of both worlds.

J. Matthew
J. Matthew
~ 3 years ago

export const store = createStore(rootReducer, [persistedState], composeEnhancers( applyMiddleware(thunk)) )

Maybe the square braces around persistedState are causing the issue? In the lesson, it's createStore(todoApp, persistedState) (no square braces).