Redux: why using Object.assign if it is not perform deep clone?

4.5k views Asked by At

One core concept in Redux is, states are immutable. However, I saw many examples, including in Redux docs using javascript Object.assign. Then, I saw this warning in MDN:

For deep cloning, we need to use other alternatives because Object.assign() copies property values. If the source value is a reference to an object, it only copies that reference value.

So, why using Object.assign if the whole point is immutability? Am I missing something here?

3

There are 3 answers

0
T.J. Crowder On BEST ANSWER

Let's look at the example you linked:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

Yes, that's a shallow copy of state, creating a new object with everything from the old but with an updated visibilityFilter. But if you're consistent about treating objects immutably, then it's fine if the new state and the old state share references to other immutable objects. Later, presumably, if you were to change something else, you'd follow the same pattern. At which point, the shallow copy we made above would continue to use the old one, and your new object would use the new one.

If you apply immutability all the way down, a shallow copy at the level you're modifying and all its parent levels is all you need. In the example above, the modification is at the top level, so it's just the one copy.

But what if it were deeper? Let's say you had this object:

let obj = {
    a: {
        a1: "a1 initial value",
        a2: "a2 initial value"
    },
    b: {
        b1: "b1 initial value",
        b2: "b2 initial value"
    }
};

If you wanted to update a, that's just like the state example above; you'd do so with just one shallow copy of obj:

obj = Object.assign({}, obj, {a: {a1: "new a1", a2: "new a2"}});

or with spread properties (currently at Stage 3, usually enabled in JSX transpiler setups, will probably make ES2018):

obj = {...obj, a: {a1: "new a1", a2: "new a2"}};

But what if you just want to update a1? To do that, you need a copy of a and of obj (because if you don't copy obj, you're modifying the tree it refers to, violating the principal):

obj = Object.assign({}, obj, {a: Object.assign({}, obj.a, {a1: "updated a1"})});

or with spread properties:

obj = {...obj, a: {...obj.a, a1: "updated a1"}};
0
intentionally-left-nil On

Redux is just a data store. As such, in its purest sense, redux doesn't really need either immutability or deep cloning to work as a concept.

However, redux requires immutability in order to work well with UI frameworks that build on top of it (such as React).

For this simple reason: What parts of my state have changed between the last time a framework looked?

Given that goal, can you see how deep-cloning actually doesn't help? If you look at one object that has been deep cloned, then every sub-part of that object is now different in terms of identity (===).

As a concrete example, if you run the following code:

const bookstore = { name: "Jane's books", numBooks: 42 };
const reduxData = { bookstore, employees: ['Ada', 'Bear'] };

And now let's say you want to change just the number of books you have at the bookstore.

If you did a deep copy, like so:

const reduxClone = JSON.parse(JSON.stringify(reduxData));
reduxClone.bookstore.numBooks = 25;

Then you would see that both the bookstore, and the employees are now different:

console.log(reduxData.bookstore === reduxClone.bookstore); // returns false
console.log(reduxData.employees === reduxClone.employees); // returns false, but we haven't changed the employees

This is a problem because it looks like everything has changed. And now React has to re-render everything to see if anything has changed.

The correct solution is to use a simple rule of immutability. If you change a value of an object, you have to create a new copy of that object. So, since we want a new numBooks, we need to create a new bookstore. And since we have a new bookstore, we need to make a new redux store.

const newBookstore = Object.assign({}, bookstore, {numBooks: 25});
const shallowReduxClone = Object.assign({}, reduxData, {bookstore: newBookstore});

Now, you'll see that the bookstores have changed (yay!), but the employees have not (double yay!)

console.log(reduxData.bookstore === shallowReduxClone.bookstore); // returns false
console.log(reduxData.employees === shallowReduxClone.employees); // returns true

I hope this example helps. Immutability allows you to change the least amount of an object when making changes. If you guarantee you'll never change an object, then you can reuse that object in other trees that you build up. In this example, we were able to use the employees object twice, without danger, because we promised to never mutate the employees object.

0
ideaboxer On

Immutability means: I (as a developer) never assign new values to object properties; either because the compiler does not allow it, the object is frozen or I just don't do it. Instead, I create new objects.

If you always keep in mind that you should not mutate objects, and obey it, you will never modify the contents of an existing object even if you shallow copied it beforehand instead of deep-copying it (modification of existing objects is what immuting code allows to prevent, because that makes the code's behavior more easily predictable).

Thus, to create immuting code, you do not need deep copying.

Why avoid deep copying?

  • better performance of shallow copying
  • smaller memory footprint of shallow copying

Example of immuting code without the need for deep copy (behind the scenes, it uses Object.assign):

const state = { firstName: 'please tell me', lastName: 'please tell me' }
const modifiedState = { ...state, firstName: 'Bob' }

Of course, you can do it wrong if you shallow copy instead of deep copy an object:

const state = { firstName: 'please tell me', lastName: 'please tell me' }
const modifiedState = state
modifiedState.firstName = 'Bob' // violates immuting, because it changes the original state as well