Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changes to object made with Object.assign mutates source object

I have following reducer code in my react-redux app:

    case 'TOGGLE_CLIENT_SELECTION':
        const id = action.payload.id;
        let newState = Object.assign({}, state);
        newState.list.forEach((client) => {
            if (client.id == id) client.selected = !client.selected;
        });
        console.log(state.list[0].selected + ' - ' + newState.list[0].selected)
        return newState;

If I got it right - Object.assign creates brand new object, but console.log displays "true - true" of "false - false". Any thoughts why it behaves like this and how can I avoid this behavior?

like image 255
Elagin Vladislav Avatar asked Mar 28 '17 15:03

Elagin Vladislav


People also ask

Does object assign mutate the object?

assign( ) With the introduction of ES6, JS gave us the ability to use this method to copy all the properties from one object to another. THIS METHOD CAN MUTATE OBJECTS IF USED INCORRECTLY.

What does mutating an object mean?

verb (used with object), mu·tat·ed, mu·tat·ing. to change; alter. Biology. to cause (a gene, cell, etc.) to undergo an alteration of one or more characteristics: The disease mutates the retina's rod cells, and they slowly stop working. Phonetics. to change by umlaut.

Does object assign change reference?

Object. assign is copying the activities object, but still keeps references to it, so if I find a specific activity and change some property on it, it changes the original too!

How does object assign () work?

The Object.assign() method only copies enumerable and own properties from a source object to a target object. It uses [[Get]] on the source and [[Set]] on the target, so it will invoke getters and setters. Therefore it assigns properties, versus copying or defining new properties.


2 Answers

Object.assign creates a shallow clone

Object.assign will copy properties to another object. Some of those properties are copied "by value" and others are copied "by reference". Strings, numbers and booleans are "by value", while objects (including Dates) are "by reference", which means they point to the same object.

So, for example, given:

var otherState = { count: 10 };

var state = { text: "foo", otherState: otherState };

var newState = Object.assign({}, state);

newState.text = "bar";
newState.otherState.count = 9;

newState.text will have a brand new string assigned to it, and state will not be affected. But both state and newState refer to the same instance of otherState, so when you updated count to 9, both state.otherState.count and newState.otherState.count are affected:

state = {
    text = "foo"
    otherState ──────┐
  }                  │
                     │
                     │
                     ├────> { count = 9 }
                     │
newState = {         │
    text = "bar"     │
    otherState ──────┘
  }

When using Object.assign, use the rule of three: if you get to the third property, you are now working in "shared" state:

newState.otherState.count = 0;
//  ^        ^        ^
//  ╵        ╵        ╵
//  1        2        3

JSON.parse to the rescue (?)

A quick and easy work around, as suggested by Tim, is to use JSON.stringify:

let newState = JSON.parse(JSON.stringify(state));

But this is not foolproof. It'll work for some simple scenarios, but there are many scenarios that might fail. For example, circular references break JSON.stringify:

let x = { id: "x" },
y = { id: "y" };
x.y = y;
y.x = x;

// fails with: "Uncaught TypeError: Converting circular structure to JSON"
let cloneX = JSON.parse(JSON.stringify(x));

Custom cloning code to the rescue (?)

If you know the structure that you're trying to clone, then you can use your own logic to handle the copying and not fall into endless loops:

function cloneX( x ){
    const c_x = { id: x.id };
    const c_y = { id: x.y.id };
    c_x.y = c_y;
    c_y.x = c_x;
    return c_x;
}

let x = { id: "x" },
    y = { id: "y" };
x.y = y;
y.x = x;

let x2 = cloneX(x);
x2.y.id = "y2";

console.log( `x2.y.id was updated: ${x2.y.id}` );
console.log( `x.y.id is unchanged: ${x.y.id}` );

It's also conceivable that, with some creative use of WeakMap, you could come up with some logic that handles unknown data structures by tracking recursion and allowing deep copies.

NPM to the rescue (?)

It'd probably be easier to just use a library, though.

like image 129
JDB Avatar answered Oct 01 '22 21:10

JDB


True, but it's not a deep copy.

The new object contains a reference to the old list.

Here's a trick to get around it (there's more "proper" ways some might say):

JSON.stringify the original. Then JSON.parse that string. The new object will be a "deep" copy (not sure if that's really technically "deep copying"). It works fine unless your sub-types are something more complex than standard plain old JSON-acceptable stuff.

like image 20
Tim Consolazio Avatar answered Oct 01 '22 22:10

Tim Consolazio