Persist state after reducers execute with Redux

Lets consider a situation where we want to trigger an asynchronous operation AFTER a reducer fires. How might we go about it?

There are a few ways. The first thing that may come to mind is triggering this operation from inside a component. Bad!

The next idea might be subscribing to the store and watching for changes. Let's take a look at how that might look:

// register a listener to the store to track cart
// changes so that we can persist them
let currentCartItems;  
store.subscribe(() => {  
  let previousCartItems = currentCartItems;
  currentCartItems = store.getState().cart.items;
  if(previousCartItems && currentCartItems !== previousCartItems) {
    CartService.save(currentCartItems);
  }
});

This is a bit hacky. Do we really want to attach a listener to the store and monitor for changes? Where does this code live so that it is logical for furture developers to find?

These are a few of the questions that make this code smell a bit.

Thunks

The simpler and cleaner way is using redux-thunk. Redux-thunk allows us to return a function expression from our action creator. The returned function receives to arguments: dispatch and getState. These are both methods from the store.

The benefit is that we can dispatch multiple methods from a single action creator. This is obviously useful if you need to perform dispatch actions and before or after asynchronous operations (think display loading mask, etc...)

Converting the above code, our original action creator was simple:

  addItem(item) {
    return { 
      type: CART_ITEM_ADDED, 
      item 
    };
  }

We convert this into a thunk and attach the save method so we can persist the updated state!

  addItem(item) {
    return (dispatch, getState) => {
      // update the cart items
      dispatch({ type: CART_ITEM_ADDED, item });

      // save the cart
      CartService.save(cartItemsSelector(getState()));
    };
  },

Now the persistance code is logically grouped with the code that performs the updates!

Testing

The original action creator had a very simple test:

describe('.addItem()', () => {  
    it('should return CART_ITEM_ADDED', () => {
      let result = actions.addItem('fake-item');
      expect(result).to.deep.equal({ type: 'CART_ITEM_ADDED', item: 'fake-item' });
    });
  });

Lets compare this to the thunk version, which is a bit more complicated:

let dispatch;  
  let getState;

  beforeEach(() => {
    dispatch = sinon.stub();
    getState = sinon.stub();
  });

  describe('.addItem()', () => {
    it('should dispatch CART_ITEM_ADDED', () => {
      // stub get state, this is the state after our action would execute
      getState.returns({ cart: { items: ['fake-item']} });

      // generate the thunk
      let thunk = actions.addItem('fake-item');

      // call the thunk with our stubbed store
      thunk(dispatch, getState);

      expect(dispatch.calledWith({ type: 'CART_ITEM_ADDED', item: 'fake-item' })).to.be.true;
    });
    it('should persist the cart', () => {
      // stub get state, this is the state after our action would execute
      getState.returns({ cart: { items: ['fake-item']} });

      // generate the thunk
      let thunk = actions.addItem('fake-item');

      // call the thunk
      thunk(dispatch, getState);

      // verify save was called properly
      expect(CartService.save.calledWith([ 'fake-item' ])).to.be.true;
    });
  });

We have two methods here and we have to do some stubbing to make sure things work as expected.

We start by creating stubs for the dispatch and getState methods.

We will assert that dispatch was called correctly in our first test.

We will also assert that our persistance method was called with the expected state. Note that I didn't stub the selector function used, which does add a dependency on the structure of our store.

That'how we do it! Happy coding.

comments powered by Disqus