35
votes

I'm a bit confused about how to change properties inside components, let's say I have the following component:

{
    props: {
        visible: {
            type: Boolean,
            default: true
        }
    },
    methods: {
         hide() {
              this.visible = false;
         }
    }
} 

Although it works, it would give the following warning:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible" (found in component )

Now I'm wondering what the best way to handle this is, obviously the visible property is passed in when created the component in the DOM: <Foo :visible="false"></Foo>

7
Why yo you edit the prop? It should (code-wise) be controlled from one place, either from within the component og from a parent component? (You can do a combination by passing an update method as a prop, in your case the hide() could be on parent which then sends a reference to it as a prop).ArneHugo
@ArneHugo As you can see, the component itself has a method to basically "show/hide" an element. The parent can also update this property. Think of it as an alert message, the parent can control if it's visible, the component itself can remove itself.woutr_be
Yes, I see that. My suggestion is to take props visible (boolean) and hide (function). Then hide is defined on the parent, which also owns the state of visible. That way you don't edit props, but you edit the parent state, which is allowed.ArneHugo
Perhaps you can make a fiddle to show what exactly you are doing, and I can change it to show you what I mean.ArneHugo
@ArneHugo i'll put together an example soonwoutr_be

7 Answers

17
votes

Referencing the code in your fiddle

Somehow, you should decide on one place for the state to live, not two. I don't know whether it's more appropriate to have it just in the Alert or just in it's parent for your use case, but you should pick one.

How to decide where state lives

Does the parent or any sibling component depend on the state?

  • Yes: Then it should be in the parent (or in some external state management)
  • No: Then it's easier to have it in the state of the component itself
  • Kinda both: See below

In some rare cases, you may want a combination. Perhaps you want to give both parent and child the ability to hide the child. Then you should have state in both parent and child (so you don't have to edit the child's props inside child).

For example, child can be visible if: visible && state_visible, where visible comes from props and reflects a value in the parent's state, and state_visible is from the child's state.

I'm not sure if this is the behavour that you want, but here is a snippet. I would kinda assume you actually want to just call the toggleAlert of the parent component when you click on the child.

var Alert = Vue.component('alert', {
  template: `
        <div class="alert" v-if="visible && state_visible">
        Alert<br> 
        <span v-on:click="close">Close me</span>
      </div>`,
  props: {
    visible: {
      required: true,
      type: Boolean,
      default: false
    }
  },
  data: function() {
    return {
      state_visible: true
    };
  },
  methods: {
    close() {
      console.log('Clock this');
      this.state_visible = false;
    }
  }
});

var demo = new Vue({
  el: '#demo',
  components: {
    'alert': Alert
  },
  data: {
    hasAlerts: false
  },
  methods: {
    toggleAlert() {
      this.hasAlerts = !this.hasAlerts
    }
  }
})
.alert {
  background-color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo" v-cloak>
  <alert :visible="hasAlerts"></alert>

  <span v-on:click="toggleAlert">Toggle alerts</span>
</div>
5
votes

If the prop is only useful for this child component, give the child a prop like initialVisible, and a data like mutableVisible, and in the created hook (which is called when the component's data structure is assembled), simply this.mutableVisible = this.initialVisible.

If the prop is shared by other children of the parent component, you'll need to make it the parent's data to make it available for all children. Then in the child, this.$emit('visibleChanged', currentVisible) to notify the parent to change visible. In parent's template, use <ThatChild ... :visibleChanged="setVisible" ...>. Take a look at the guide: http://vuejs.org/v2/guide/components.html

5
votes

According to the Vue.js component doc:

When the parent property updates, it will flow down to the child, but not the other way around. So, how do we communicate back to the parent when something happens? This is where Vue’s custom event system comes in.

Use $emit('my-event) from the child to send an event to the parent. Receive the event on the child declaration inside the parent with v-on:my-event (or @my-event).

Working example:

// child

Vue.component('child', {
  template: '<div><p>Child</p> <button @click="hide">Hide</button></div>',
  methods: {
    hide () {
      this.$emit('child-hide-event')
    }
  },
})

// parent

new Vue({
  el: '#app',
  data: {
    childVisible: true
  },
  methods: {
    childHide () {
      this.childVisible = false
    },
    childShow () {
      this.childVisible = true
    }
  }
})
.box {
  border: solid 1px grey;
  padding: 16px;
}
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app" class="box">
  <p>Parent | childVisible: {{ childVisible }}</p>
  <button @click="childHide">Hide</button>
  <button @click="childShow">Show</button>
  <p> </p>
  <child @child-hide-event="childHide" v-if="childVisible" class="box"></child>
</div>
4
votes

After a read of your latest comments it seems that you are concerned about having the logic to show/hide the alerts on the parent. Therefore I would suggest the following:

parent

# template
<alert :alert-visible="alertVisible"></alert>

# script
data () {
  alertVisible: false,
  ...
},
...

Then on the child alert you would $watch the value of the prop and move all logic into the alert:

child (alert)

# script
data: {
  visible: false,
  ...
},
methods: {
  hide () {
    this.visible = false
  },
  show () {
    this.visible = true
  },
  ...
},
props: [
  'alertVisible',
],
watch: {
  alertVisible () {
    if (this.alertVisible && !this.visible) this.show()
    else if (!this.alertVisible && this.visible) this.hide()
  },
  ...
},
...
2
votes

To help anybody, I was facing the same issue. I just changed my var that was inside v-model="" from props array to data. Remember the difference between props and data, im my case that was not a problem changing it, you should weight your decision.

E.g.:

<v-dialog v-model="dialog" fullscreen hide-overlay transition="dialog-bottom-transition">

Before:

export default {
    data: function () {
        return {
            any-vars: false
        }
    },
    props: {
            dialog: false,
            notifications: false,
            sound: false,
            widgets: false
        },
    methods: {
        open: function () {
            var vm = this;

            vm.dialog = true;
        }
    }
}

After:

export default {
    data: function () {
        return {
            dialog: false
        }
    },
    props: {
            notifications: false,
            sound: false,
            widgets: false
        },
    methods: {
        open: function () {
            var vm = this;

            vm.dialog = true;
        }
    }
}
0
votes

Maybe it looks like on hack and violates the concept of a single data source, but its work) This solution is creating local proxy variable and inherit data from props. Next work with proxy variable.

Vue.component("vote", {
    data: function() {
        return {
            like_: this.like,
            dislike_: this.dislike,
        }
    },

    props: {
        like: {
            type: [String, Number],
            default: 0
        },
        dislike: {
            type: [String, Number],
            default: 0
        },
        item: {
            type: Object
        }
    },

    template: '<div class="tm-voteing"><span class="tm-vote tm-vote-like" @click="onVote(item, \'like\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{like_}}</span></span><span class="tm-vote tm-vote-dislike" @click="onVote(item, \'dislike\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{dislike_}}</span></span></div>',

    methods: {
        onVote: function(data, action) {
            var $this = this;
            // instead of jquery ajax can be axios or vue-resource
            $.ajax({
                method: "POST",
                url: "/api/vote/vote",
                data: {id: data.id, action: action},
                success: function(response) {
                    if(response.status === "insert") {
                        $this[action + "_"] = Number($this[action + "_"]) + 1;
                    } else {
                        $this[action + "_"] = Number($this[action + "_"]) - 1;
                    }
                },
                error: function(response) {
                    console.error(response);
                }
            });
        }
    }
});

use component and pass props

<vote :like="item.vote_like" :dislike="item.vote_dislike" :item="item"></vote>
0
votes

I wonder why it is missed by others when the warning has a hint

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible" (found in component )

Try creating a computed property out of the prop received in the child component as

computed: {
  isVisible => this.visible
}

And use this computed in your child component as well as to emit the changes to your parent.