Changes to object made with Object.assign mutates source object

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.


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.