Search code examples
javascriptreduxlodash

Redux state is being mutated with lodash's mergeWith


I am using lodash's mergeWith to merge some payload data into some of my redux state. However, when doing this, I end up directly mutating state. I don't understand how this is happening, since I am using {...state} to make the merge occur. Why is this happening and what can I do to not mutate my state directly? You can see the below snippet for an example of what is happening. Thanks!

const merger = (objectOne, objectTwo) => {
  const customizer = (firstValue, secondValue) => {
    return _.isArray(firstValue) ? secondValue : undefined;
  };

  return _.mergeWith(objectOne, objectTwo, customizer);
};

const state = {
  1: {a: true, b: true, c: true},
  2: {a: true, b: true, c: true},
  3: {a: true, b: true, c: true},
}

const payload = {
   2: {a: true, b: false, c: true},
}

console.log("Merged data:");
console.log(merger({...state}, payload));
console.log("Manipulated state:");
console.log(state);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>


Solution

  • Here is in a nut shell the issue:

    let a = { foo: 'A'}
    let c = { ... a }  // shallow copy of a
    
    a.foo = 'boo'
    
    console.log(a)
    console.log(c) // works as expected c.foo is NOT changed and still is 'A'

    As you can see from the above example with spreading and value based properties shallow copy works as expected. However when you do this:

    let x = { foo: { boo: 'A' }}  // object as value this time
    let y = { ... x }  // shallow copy of x
    
    x.foo.boo = 'beer'
    
    console.log(x.foo.boo)
    console.log(y.foo.boo) // should be 'boo' but it is 'beer'

    Shallow copy does not work as well since the clone has references pointing to the old x objects instead of cloned ones.

    To remedy this and also to to make your code somewhat more concise you could:

    const state = { 1: {a: true, b: true, c: true}, 2: {a: true, b: true, c: true}, 3: {a: true, b: true, c: true}, }
    const payload = { 2: {a: true, b: false, c: true} }
    
    const merger = (...args) => _.mergeWith(...args, (a,b) => _.isArray(a) ? b : undefined)
    
    console.log("Merged data:");
    console.log(merger(_.cloneDeep(state), payload));
    console.log("Manipulated state:");
    console.log(state);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

    First switch ot lodash _.cloneDeep which would deep copy your entire object tree and also you can make your merge method more concise with ES6 spread etc.