Search code examples
javascriptvue.jssynchronizationparent-childprop

Vue.js - prop sync not instant


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


Solution

  • 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.