Redux: Persisting the State to the Local Storage

Share this video with your friends

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.

Pavel Dolecek
Pavel Dolecek
~ 8 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
~ 8 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
~ 8 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
~ 8 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
~ 8 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 Abramov(instructor)
~ 8 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 Abramov(instructor)
~ 8 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 Abramov(instructor)
~ 8 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
~ 8 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
~ 8 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
~ 8 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
~ 8 years ago

I think the font is Operator Mono

J. Matthew
J. Matthew
~ 5 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
~ 5 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).