Search code examples
javascriptreact-redux

Invariant Violation: A state mutation was detected between dispatches, in the path


I would like to concatenate one item in the array with another one and remove the second item from the array. When I tried the old way, I get the state mutation detected error. When I tried the Object.assign, I am unable to get the values concatenated.

EDIT: What is the equivalent of interests[makeIndex].value = ``${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}`` using Object.assign?

For e.g., for the below code segment the output I expect is,

Preferred Make - before Chevrolet
Preferred Make - after Chevrolet, Porsche
// Interests array from state
let interests = [
  {
    type: 'Preferred Make',
    value: 'Chevrolet',
  },
  {
    type: 'Second Preferred Make',
    value: 'Porsche',
  },
  {
    type: 'Preferred Model',
    value: 'Corvette',
  },
  {
    type: 'Second Preferred Model',
    value: 'Macan',
  }
];

console.log("Preferred Make - before", interests[0].value);

const secondPreferredMakeIndex = interests
  .map(x => x.type)
  .indexOf('Second Preferred Make');

if (secondPreferredMakeIndex > -1) {
  let makeIndex = interests.map(x => x.type).indexOf('Preferred Make');

  if (makeIndex > -1) {
    // For the below, I get mutation error. But it works
    // interests[makeIndex].value = `${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}`;

    // Concatenate and use Object.assign to avoid mutation
    interests = Object.assign([], interests, { makeIndex: `${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}` });
    /*
    // Tried the below as well in vain
    interests = Object.assign([], interests, {
      makeIndex: {
        type: 'Preferred Make',
        value: `${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}`
      },
    });
    */
  }

  // Delete the second Preferred Make
  interests.splice(secondPreferredMakeIndex, 1);
}

console.log("Preferred Make - after", interests[0].value);

Appreciate the helps


Solution

  • Issues

    Anything you select from the Redux store is still a reference to that object in the store. The UI should not mutate these references!

    • interests[makeIndex].value = `${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}`; mutates the interests[makeIndex].value state value.

    • The interests = Object.assign([], interests, { makeIndex: `${interests[makeIndex].value}, ${interests[secondPreferredMakeIndex].value}` }); is also a mutation since it re-assigns the entire interests array value.

    The code also creates a new makeIndex property on the object instead of updating the value property.

    Solution

    You should create a "deep copy" (copy the array and array elements into new object references) of the selected interests state since you want to update specific elements and remove elements from the array. For demonstration purposes I'm simply using JSON.parse(JSON.stringify(interests)) to "clone" the object.

    Use the value key instead of makeIndex for the element object you want to update.

    Object.assign(
      interestsCopy[makeIndex],
      {
        value: `${interestsCopy[makeIndex].value}, ${interestsCopy[secondPreferredMakeIndex].value}`
      }
    );
    

    // Interests array from state
    let interests = [
      {
        type: 'Preferred Make',
        value: 'Chevrolet',
      },
      {
        type: 'Second Preferred Make',
        value: 'Porsche',
      },
      {
        type: 'Preferred Model',
        value: 'Corvette',
      },
      {
        type: 'Second Preferred Model',
        value: 'Macan',
      }
    ];
    
    // "deep copy"
    let interestsCopy = JSON.parse(JSON.stringify(interests));
    
    console.log("Preferred Make - before (orig)", interests[0].value);
    console.log("Preferred Make - before (copy)", interestsCopy[0].value);
    
    const secondPreferredMakeIndex = interestsCopy
      .map(x => x.type)
      .indexOf('Second Preferred Make');
    
    if (secondPreferredMakeIndex > -1) {
      let makeIndex = interestsCopy.map(x => x.type).indexOf('Preferred Make');
    
      if (makeIndex > -1) {
        Object.assign(
          interestsCopy[makeIndex],
          {
            value: [
              interestsCopy[makeIndex].value,  
              interestsCopy[secondPreferredMakeIndex].value
            ].join(", "),
          }
        );
      }
    
      // Delete the second Preferred Make
      interestsCopy.splice(secondPreferredMakeIndex, 1);
    }
    
    console.log("Preferred Make - after (orig)", interests[0].value);
    console.log("Preferred Make - after (copy)", interestsCopy[0].value);

    A more correct solution may be to map the current interests array to a new array with a new element object that you want to update, then filter the old element out of the array.

    Example:

    // Interests array from state
    let interests = [
      {
        type: 'Preferred Make',
        value: 'Chevrolet',
      },
      {
        type: 'Second Preferred Make',
        value: 'Porsche',
      },
      {
        type: 'Preferred Model',
        value: 'Corvette',
      },
      {
        type: 'Second Preferred Model',
        value: 'Macan',
      }
    ];
    
    const interestsCopy = interests.map((el, index, arr) => {
      const { type, value } = el;
      switch(type) {
        case "Preferred Make":
          return {
            type,
            value: [
              value,
              arr.find(el => el.type === "Second Preferred Make")?.value
            ]
              .filter(Boolean)
              .join(", "),
          };
    
        default:
          return el;
      }
    }).filter(({ type }) => type !== "Second Preferred Make");
    
    console.log("Preferred Make - after (orig)", interests[0].value);
    console.log("Preferred Make - after (copy)", interestsCopy[0].value);