BlockChange
BlockChange

Reputation: 345

Redux: Add/remove from nested object/array, without mutating?

I thought assign was supposed to make a new object, that's why I did this in my reducer:

    case types.ADD_ATTRIBUTE:
      var newState = Object.assign({}, state)
      newState.attributes[action.industry].push(action.attribute)
      return Object.assign({}, state, newState);

    case types.REMOVE_ATTRIBUTE:
      var newState = Object.assign({}, state)
      var removeIndex = newState.attributes[action.industry].indexOf(action.attribute)
      newState.attributes[action.industry].splice(removeIndex, 1)
      return Object.assign({}, state, newState);

However, when I do this, the component will not trigger an update (componentWillReceiveProps). It does receive the new props, but the react-redux internal shouldComponentUpdate does not detect changes.

What am I doing wrong here?

Upvotes: 3

Views: 1949

Answers (4)

Igorsvee
Igorsvee

Reputation: 4201

Here's how you could handle the types.ADD_ATTRIBUTE case:

With Object.assign:

const newActionIndustry = state.attributes[action.industry].concat(action.attribute)

const newAttributes = Object.assign({}, state.attributes, {
  [action.industry]: newActionIndustry
})

const newState =  Object.assign({}, state, {
  attributes: newAttributes
})

Handle the types.REMOVE_ATTRIBUTE case by yourself using this code.

Upvotes: 0

Manolo Santos
Manolo Santos

Reputation: 1913

React-redux's shouldComponentUpdate() does a shallow comparison of the state in order to decide to render or not. This shallow comparison checks only one level depth of the object, it means that if you don't change the reference of the state itself or any of its first-level properties, it won't trigger an update of the component.

Your array is deeply nested in state.attributes[action.industry], and your action is not modifying state nor attributes, so react-redux won't update your component. In order to solve your problem, you need to change attributes[action.industry], creating a new array (e.g. using Array.concat() instead of Array.push() or using the spread operator like in attributes[action.industry] = [...attributes[action.industry], action.attribute ]

Alternatively, if you are using a stateful component you can create your own version of shouldComponentUpdate() that takes into account the attributes property in order to decide to render or not.

Upvotes: 0

BlockChange
BlockChange

Reputation: 345

I eventually settled on this: (with some ES6 magic)

  case types.ADD_ATTRIBUTE:
      let newAttrState = state.attributes[action.industry].slice()
      newAttrState.push(action.attribute)
      return Object.assign({}, state, { attributes: { [action.industry]: newAttrState }} );

  case types.REMOVE_ATTRIBUTE:
      var newAttrState = state.attributes[action.userIndustry].slice()
      let removeIndex = newAttrState.indexOf(action.attribute)
      newAttrState.splice(removeIndex, 1)
      return Object.assign({}, state, { attributes: { [action.userIndustry]: newAttrState }} );

*Update: Which I now realize, is overriding the whole attributes object, with only one dynamically keyed array, while I need to sustain the other arrays stored in that object...

Upvotes: 0

elmeister
elmeister

Reputation: 184

In case if you want to re-render object containing attributes[action.industry], you need to recreate this array same as you do with state.

case types.ADD_ATTRIBUTE:
  return {
    ...state,
    attributes: {
      ...state.attributes,
      [action.industry]: [...state.attributes[action.industry], action.attribute]
    }
  }

case types.REMOVE_ATTRIBUTE:
  const removeIndex = newState.attributes[action.industry].indexOf(action.attribute)
  return {
    ...state,
    attributes: {
      ...state.attributes,
      [action.industry]: [
          ...state.attributes[action.industry].slice(0, removeIndex), 
          ...state.attributes[action.industry].slice(removeIndex + 1)
        ]
      }
   }

Upvotes: 4

Related Questions