0

I am seeing some weird behaviour here that was unexpected, but it makes intuitive-sense to me in terms of pure JavaScript.

I have a form controller that accumulates a this.thing object that is sent to the server on the final submit. It's a multi-step form, so each step adds some data to this.thing.

So the controller has:

data() {
    return {
        thing: {},
    };
},

The DOM markup for this controller has a child like:

<a-child
    :initial-thing="thing"
></a-child>

The child uses that prop to display its initial state, so it receives the prop and sets it into its own local state as instance data:

initialThing: {
    type: Object,
    required: true,
},

...

data() {
    return {
        thing: this.initialThing,
    };
},

Then this child has a checkbox that is like this:

<a-checkbox
    v-model="thing.field"
    :initial-value="initialThing.field"
></a-checkbox>

This all works fine, except I just noticed that when the checkbox changes, it's mutating the parent controllers thing.field value.

I'm making this question because I don't understand how Vue can do that, and the only thing that makes sense to me is that when the child does thing: this.initialThing, it's allowing the child to call the setter function on that field on this.initialThing.

It stops mutating the parent's state if I do this instead:

data() {
    return {
        thing: { ...this.initialThing },
    };
},

In my actual app, it's more complex because there are 2 intermediate components, so the grandchild is mutating the grandparent's state, and it stems from the pattern I am describing here.

Can anyone provide a kind of textbook answer for what is happening here? I'm hesitant to rely on this behaviour because the code driving it is not explicit. It makes some of my $emit() events redundant in favour of using this indirect/non-explicit way of sending data upstream.

Also to be clear, this has nothing to do with v-model because it also does it if I do this.thing.field = 'new value';. I believe it has everything to do with inheriting the getters/setters on this.initialThing. Is it safe to rely on this behaviour? If I rely on it, it will make my code more concise, but a naive individual may have a hard time understanding how data is making it into the grandparent component.

agm1984
  • 15,500
  • 6
  • 89
  • 113
  • 1
    I suppose thing is passed by reference. Would not rely on it as it breaks self containment of components making it more difficult to maintain. – cYrixmorten Sep 08 '19 at 07:36
  • 1
    Check this SO question for a more in depth answer : https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object – BTL Sep 08 '19 at 10:27

1 Answers1

1

This is a shallow copy so you can't prevent mutating grandchildren.

data() {
    return {
        thing: { ...this.initialThing },
    };
},

The solution is below:

data() {
    return {
        thing: JSON.parse(JSON.stringify(this.initialThing)),
    };
},

const initialThing = {
  age: 23,
  name: {
    first: "David",
    last: "Collins",
  }
}

const shallowCopy = { ...initialThing };

shallowCopy.age = 10;
shallowCopy.name.first = "Antonio"; // will mutate initialThing

console.log("init:", initialThing);
console.log("shallow:", shallowCopy);

const deepCopy = JSON.parse(JSON.stringify(initialThing));
deepCopy.age = 30;
shallowCopy.first = "Nicholas"; // will not mutate initialThing

console.log("------Deep Copy------");
console.log("init:", initialThing);
console.log("deep:", deepCopy);

How it works:

JSON.stringify(this.initialThing)

This converts JSON Object into String type. That means it will never mutate children anymore. Then JSON.parse will convert String into Object type.

But, using stringify and parse will be expensive in performance. :D

UPDATED: If you are using lodash or it is okay to add external library, you can use _.cloneDeep.

_.cloneDeep(value); // deep clone
_.clone(value); // shallow clone
Diamond
  • 3,470
  • 2
  • 19
  • 39
  • I will mark your answer as correct if you can re-work it to show that shallow cloning with `Object.assign({}, obj)` or spread operator is viable, but the best way is to deep clone. I'm going to use lodash cloneDeep for this purpose. I likely wouldn't ever use JSON stringify&parse, but it is an option. This URL here is helpful https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/10916838#10916838 as is the one linked by BTL in a comment on the original question. – agm1984 Sep 12 '19 at 01:41
  • The interesting thing from the link I provided is the native deep cloning. This is something we should be tracking in questions related to mine. I will give you a bonus point in real life if you include `import cloneDeep from 'lodash/cloneDeep';` to ensure people aren't importing the entire Lodash library. – agm1984 Sep 12 '19 at 01:52