2

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

koga73
  • 712
  • 6
  • 13

1 Answers1

1

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.

thanksd
  • 44,567
  • 20
  • 126
  • 130
  • Did you mean this.value instead of this.temp = val; ? Also I can see how this approach works, but doesn't that kind of defeat the purpose of syncing a value between parent and child now that I have to maintain a local copy and keep that in sync as well? – koga73 Sep 20 '18 at 17:30
  • Yes I did. And yeah it's more boilerplate code, but I don't think it defeats the purpose of the value being synced. The parent component still gets the synced value automatically, it doesn't care how. I personally would use a computed with a get/set and then wait for nextTick, but watchers isn't a bad option either. Just depends on what you want to do. – thanksd Sep 20 '18 at 17:36
  • @thanksd "The only way to wait for the properties of the parent and child to be in sync is to call the $neckTick handler" - Isn't `nextTick`'s callback called AFTER the DOM has been updated? What has emiting the event (to update to the data) have to do with the DOM of the child component? https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue – Bennett Dams Sep 20 '18 at 17:55
  • @thanksd Does your code not get stuck in a "watch / update" loop? Local variable updates, emits event, parent variable updates, watch triggers local variable again which emits event again, etc – koga73 Sep 20 '18 at 18:06
  • @koga73, nope! Watchers only get called when the value changes. You can run the code snippet and see for yourself. – thanksd Sep 20 '18 at 18:13
  • @BennettDams, I didn't qualify that statement enough. I'll edit the post to better explain. – thanksd Sep 20 '18 at 18:16