1

I'm trying to wrap my wits around the semantics of Vue props and data properties. In the code below, the item child component accepts newItem from the parent. The child component defines item (sorry for the ambiguity) as this.newItem. The parent passes in newItem instead of item in order to get around the prohibition of modifying parent component values directly.

The console shows no warnings, but I'm wondering if only because the Vue rendering(?) machinery doesn't recognize a violation. The child item can be seen in dev tools to be simply creating a reference to the passed-in newItem, so in essence the prop is still getting modified directly.

Should I be initializing the child item using the Item constructor, instead? Or must the child instead issue some kind of 'cancel-edit` event which is handled by the parent?

cancelEdit assigns _cachedItem to item, which is the same(?) as assigning to newItem:

// An item
Vue.component('item', {
    props: [
      'newItem'
    ],
    data: function() {
      return {
        item: this.newItem,
        editing: false,
        _cachedItem: null
      }
    },
    methods: {
      startEdit: function() {
        debugger
        this._cachedItem = new Item(this.item.id, this.item.text);
        this.editing = true;
      },
      cancelEdit: function() {
        debugger
        this.item = this._cachedItem;
        this._cachedItem = null;
        this.editing = false;
      },
      finishEdit: function() {
        debugger
        this.editing = false;
      },
    },
...

Parent template:

Vue.component('items', {
    props: {
      'items': {
        type: Array,
      },
      'item-type': {
      type: String
    }
    ...
        <item
            v-for="(item, index) in items"
            v-bind:newItem="item"
            v-bind:key="item.id"
            v-on:remove="removeItem(index)" />
    ...
Tom Russell
  • 1,015
  • 2
  • 10
  • 29

1 Answers1

1

In JavaScript objects are passed by reference. The Vue docs clearly state ..

Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.

If you'd like to avoid this behavior you can create deep clones of your objects. Something like this ..

item: JSON.parse(JSON.stringify(this.newItem)),

Which would create a completely independent local copy of your object. If you'd like to keep both objects in sync then you can communicate your intent to mutate a value to the parent through events and have it update it's own copy of an object. An elegant way to handle this would be by using the .sync modifier.

tony19
  • 125,647
  • 18
  • 229
  • 307
Husam Ibrahim
  • 6,999
  • 3
  • 16
  • 28
  • @Husan. Thanks. This seems to be what I'm looking for. But can you elaborate/clarify "...communicate your intent to mutate a value to the parent to update it's own copy of an object through events..."? – Tom Russell Oct 14 '18 at 07:47
  • I suppose this means that the Vue tooling needs to be updated to verify a child isn't modifying a shallow copy, probably by checking whether the object of an assignment is equivalent to the prop passed into the child. – Tom Russell Oct 14 '18 at 07:51
  • 1
    @Tom In Vue you can define your own [custom events](https://vuejs.org/v2/guide/components-custom-events.html). When you `$emit` a custom event you can send any custom data as a payload (for example a child's value of some object/property or a modification thereof). A parent can listen to this event and receive the payload and do anything it chooses to do with it (for example modify it's own copy of an object). – Husam Ibrahim Oct 14 '18 at 07:53
  • @Husan: This seems pretty elegant. Do you prefer this approach? – Tom Russell Oct 14 '18 at 07:54
  • 1
    @Tom It is indeed the preferred approach and the one recommended by the docs. RE: tooling it could be an intentional design choice by the devs so as to not limit developer freedom as sometimes it could be an intended behavior when the dev knows what they're doing. – Husam Ibrahim Oct 14 '18 at 07:56
  • 1
    @Tom No harm in that. But be careful because it might bite you in the back when some child modifies an object it received as a prop and you don't know where that mutation is coming from. – Husam Ibrahim Oct 14 '18 at 07:58
  • @Husan OK. I suppose there might be use case wherein the displayed value of a child component should get overwritten by a more persistent one. Yes? – Tom Russell Oct 14 '18 at 07:58
  • @Husan I understand the problem with muting a prop in the child is, rather, that it doesn't "stick" but instead gets overwritten by the parent's value on the next re-render. Perhaps I misread something. – Tom Russell Oct 14 '18 at 08:02
  • 1
    @Tom indeed that's the case with primitives. But objects and arrays are different. Any change in the child is reflected in the parent and vice-versa because both include a reference to the same object/array. Nothing gets overwritten. – Husam Ibrahim Oct 14 '18 at 08:06
  • 1
    @Husan Thanks! I get it, now. I'm leaning toward the event model, but will check out the `.sync` modifier as you suggested. – Tom Russell Oct 14 '18 at 08:21
  • 1
    @Tom Primitives vs reference props is indeed a common 'gotcha' and I've seen quite a few questions here where this was the root problem. But after a while you learn to keep an eye for these sorts of things. RE: `.sync` modifier it's just syntactic sugar in the event model. – Husam Ibrahim Oct 14 '18 at 08:24
  • 1
    I finally figured out the semantics of the child's emission of `cancel`: emit `_cachedItem` and the the item's index with the event. The index is available by making it a prop. The parent then restores the value of the item. – Tom Russell Oct 19 '18 at 15:50
  • @Tom Yes that seems like a clean way to implement this whilst keeping things separate. – Husam Ibrahim Oct 19 '18 at 17:06
  • Incidentally I came across [this case](https://stackoverflow.com/questions/52874769/vue-props-passed-to-router-link/52876913) where the person wanted to pass a prop through a Vue router-link to a child component. His problem was that the prop gets passed as a one off event when the router link is activated but doesn't get updated as the parent's value changes. In his case making the prop an object was a viable solution since it get's passed by reference and then the child will have access to the value as it gets updated by the parent. – Husam Ibrahim Oct 19 '18 at 17:06
  • 1
    Nice. Maybe this should be included in "Edge Cases". LOL. – Tom Russell Oct 21 '18 at 08:01