Using vue.js to sync a prop between parent and child. The problem is sync uses events and every time I change the value I have to wait for $nextTick before the value updates. This is not ideal because I do not want to put $nextTick every time I change the value. Is there a way to make the event / prop update happen immediately?
HTML:
<div id="app">
<foo inline-template v-bind:bar.sync="bar">
<div>
<button v-on:click="handler_button_click">Set to 5</button>
</div>
</foo>
<span>bar: {{bar}}</span>
</div>
JS:
const EVENT_UPDATE_BAR = "update:bar";
Vue.component("foo", {
props:["bar"],
computed:{
_bar:{
get:function(){
return this.bar;
},
set:function(value){
//Mutating the prop here solves the problem, but then I get a warning about mutating the prop...
//this.bar = value;
this.$emit(EVENT_UPDATE_BAR, value);
}
}
},
methods:{
handler_button_click:function(){
//It seems that $nextTick must run before value is updated
this._bar = 5;
//This shows old value - event / prop has not fully propagated back down to child
alert("bar: " + this._bar);
}
}
});
new Vue({
el:"#app",
data:{
bar:1
}
});
See working example on CodePen: https://codepen.io/koga73/pen/MqLBXg
Let's take a look at your example. I've added watchers for the value of the bar
property for both the parent and child components and console.log
statements to note different points in the transfer of data between the two components:
const EVENT_UPDATE_BAR = "update:bar";
Vue.component("foo", {
props:["bar"],
computed:{
_bar:{
get:function(){
console.log('_bar getter is called')
return this.bar;
},
set:function(value){
console.log('_bar setter is called')
this.$emit(EVENT_UPDATE_BAR, value);
}
}
},
methods:{
handler_button_click:function(){
console.log('handler_button_click is called')
this._bar = 5;
console.log('this._bar is accessed with value: ', this._bar);
this.$nextTick(() => {
console.log('next tick handler is called')
})
console.log('handler_button_click finishes')
}
},
watch: {
bar() {
console.log('child bar watcher is called')
}
}
});
new Vue({
el:"#app",
data:{
bar:1
},
watch: {
bar() {
console.log('parent bar watcher is called')
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<foo inline-template v-bind:bar.sync="bar">
<div>
<button v-on:click="handler_button_click">Set to 5</button>
</div>
</foo>
<span>bar: {{bar}}</span>
</div>
You'll notice that handler_button_click
fires first, followed by the get
and set
methods for the _bar
computed. But, the two watchers for bar
don't fire until after the handler_button_click
function finishes. This is indicating that the value being passed by the child's $emit
call does not get handled by the parent component until after the handler_button_click
function has finished executing.
The only way that Vue provides to wait for the properties of the parent and child to be in sync within the handler_button_click
function is to call $nextTick
, as you described. The $nextTick
function will wait to execute its handler until the DOM has finished updating. Since the DOM won't finish rendering until all of the data changes in the parent and child component are resolved, you can be sure that everything will be in sync at this point.
All that said, it seems like you simply want for the child's _bar
property to update immediately when it is set, and you don't necessarily need the value to have updated in the parent scope immediately.
In that case, you could use watchers instead of the computed getter and setter. This way, since the child's property isn't computed, it will update immediately and the parent's property will be updated after the next tick.
Here's an example:
const EVENT_UPDATE_BAR = "update:bar";
Vue.component("foo", {
props: ["bar"],
data() {
return {
value: this.bar,
};
},
methods: {
handler_button_click() {
this.value = 5;
alert("bar: " + this.value);
}
},
watch: {
value(val) {
this.$emit(EVENT_UPDATE_BAR, val);
},
bar(val) {
this.value = val;
}
}
});
new Vue({
el: "#app",
data() {
return {
bar: 1
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<foo inline-template v-bind:bar.sync="bar">
<div>
<button v-on:click="handler_button_click">Set to 5</button>
</div>
</foo>
<span>bar: {{bar}}</span>
</div>
Note that I've changed the name of the _bar
property to value
, since properties prepended with an underscore can't be watched as they are not proxied to the Vue instance.