33
votes

I'm trying to wrap my head around how to change deeply-nested state in redux. It makes sense to me to combine reducers and change first-level properties of the state for those pieces. Where I'm not so clear is on how to change the state of a deeply-nested property. Let's pretend that I have a shopping cart app. Below is my state:

{
    cart: {
      items: []
    },
    account: {
      amountLeft: 100,
      discounts: {
        redeemed: [],
        coupons: {
          buyOneGetOne: false
        }
      }
    }  
}

When a user enters in a code, let's say they can redeem the "buyOneGetOne" coupon and that value should become true. I have a reducer for cart and another for account. For a first-level property (like if I was clearing the cart's items), I would just do the following in my reducer:

case 'EMPTY_CART':
  return Object.assign({}, state, {items: []});

For changing buyOneGetOne, however, it seems like I would first need to do an Object.assign on coupons (because buyOneGetOne was modified), then doing an Object.assign on discounts (because I had modified coupons), and then finally emitting the action so the reducer could do Object.assign on the account (because discounts has now changed). This seems really complicated and easy to do wrong, though, which leads me to believe there must be a better way.

Am I going about this all wrong? It seems like reducers are only used to modify the root level properties of the state (like cart and account) and that I shouldn't have a reducer that touches state inside the account (like a discounts reducer), because account already has a reducer. But when I only want to change one property far down the state tree, it gets complex to merge every object from that change all the way up the object chain to the child of the root...

Can you/should you have reducers inside of reducers, like in this case have a discounts reducer?

3

3 Answers

34
votes

You can definitely have reducers inside of reducers; in fact, the redux demo app does this: http://redux.js.org/docs/basics/Reducers.html.

For example, you might make a nested reducer called `discounts':

function discounts(state, action) {
    switch (action.type) {
        case ADD_DISCOUNT:
            return state.concat([action.payload]);

        // etc etc
    }
}

And then make use of this reducer in your account reducer:

function account(state, action) {
    switch (action.type) {
        case ADD_DISCOUNT:
            return {
                ...state,
                discounts: discounts(state.discounts, action)
            };

         // etc etc
    }
}

To learn more, check out the egghead.io redux series by Dan himself, specifically the reducer composition video!

5
votes

you should nest combinedReducers for each level deep

0
votes

Yes, separating those reducers is a right thing. Actually, if you are afraid that some of those actions will require change of other reducer, you can either react to other constant, or just dispatch another action.

I've created a library to address this issue -- to allow easy composition, and to avoid verbose syntax, along with merging result. So, your example will look like that:

import { createTile, createSyncTile } from 'redux-tiles';

const cartItems = createSyncTile({
  type: ['account', 'cart'],
  fn: ({ params }) => ({ items: params.items })
});

const coupons = createSyncTile({
  type: ['account', 'coupons'],
  fn: ({ params }) => params,
});

const reedemedCoupons = createSyncTile({
  type: ['account', 'reedemedCoupons'],
  fn: ({ params }) => params.coupons
});

const account = createTile({
  type: ['account', 'info'],
  fn: ({ api }) => api.get('/client/info')
});

Using this strategy each component is atomic, and you can easily create new and dispatch others, without affecting others, it makes it easier to refactor and remove some functionality in the future, when your "tiles" are following single responsibility principle.