Update data when prop changes. Data derived from prop

My component is such

export default {
  props: {
    day_exr: {
      type: Object,
      required: true
    }
  },

  data: function () {
    return {
      id: this.day_exr.id,
      weight: this.day_exr.weight,
      auto_weight: Boolean(this.day_exr.auto_weight),
      auto_reps: Boolean(this.day_exr.auto_reps),
      day_id: this.day_exr.day_id,
      exercise_id: this.day_exr.exercise_id.toString(),
      increment: this.day_exr.increment,

As you see the data keys are derived from day_exr prop. However when day_exr prop changes (parent passes in a new day_exr), the data does not update.

To make the data update on prop change, i have to make a watcher and do this

  watch: {
    day_exr: function (newExrDay) {
      this.id = newExrDay.id
      this.weight = newExrDay.weight
      this.auto_weight = Boolean(newExrDay.auto_weight)
      this.auto_reps = Boolean(newExrDay.auto_reps)
      this.day_id = newExrDay.day_id
      this.exercise_id = newExrDay.exercise_id.toString()
      this.increment = newExrDay.increment
    }
  },

I feel this is very messy. Is there a better way to do this? Perhaps re-render the component on prop change so the data get re-assigned?

2 Likes

I would simply clone the object if you use (almost) all of its properties. Either using JSON.parse(JSON.stringify(this.day_extr)), or use a util like clonedeep.

If the object does not have arrays or other objects nested (so it’s only strings, numbers and other basic types), you can also do Object.assign({}, this.day_extr)

the only important thing is that this means you will have to use another object:

data:() {
  return {
   dayExpr: JSON.parse(JSON.stringify(this.day_extr))
    // or
    dayExpr: clonedeep(this.day_expr)
    // or 
   dayExpr: Object.assign({}, this.day_extr)
  }
}

…and do the same in the watch

2 Likes

In this case why clone the prop? Why not just

data:() {
  return {
   dayExpr: this.day_extr
  }
}

Then in watch

  watch: {
    day_extr: function () {
      this.dayExpr= this.day_extr
    }
  },

Any special reason you are cloning the prop?

5 Likes

Not really. think I got it mixed up with another usecase in my head (editing a copy before before applying changes to the original).

any explanation on why the data derived from the props are not updated when the prop is updated ?

1 Like

The people here say that’s how Vue is supposed to work. Expected behavior. So you have to manually watch it like how I did.

1 Like

@armesh I found that computed is also working for that purpose.

1 Like

You mean to say you made a computed property equal to the passed in prop? That’s also a viable solution I think.

I believe that I have encountered the same problem. @armesh mentions watching it above but I find that watching a nested object doesn’t work (and I’m not at all sure if using a computed value is really a solution).

(By the way, I asked in this post about errors returned on primitives and objects and assume that this could all be related.)

For example:

HTML:

<div id="main">
    <button @click="x_toggle()">Toggle</button>
    <x-comp :comp1="val1" :comp2="val2"></x-comp>
</div>

Component:

Vue.component("x-comp",
{
    props:
    {
        comp1: Boolean,
        comp2: Object
    },

    // VERSION 1
    //template: `<div v-show="localcomp1">val1 is true</div>`,
    // VERSION 2
    //template: `<div v-show="localcomp2.val3">val3 is true</div>`,

    data: function()
    {
        return {
            localcomp1: this.comp1,
            localcomp2: JSON.parse(JSON.stringify(this.comp2))
        }
    },

    watch:
    {
        // VERSION 1
        //comp1: function()
        //{
        //this.localcomp1 = this.val1;
        //}
        // VERSION 2
        //comp2: function()
        //{
        //    this.localcomp2 = this.val2;
        //}
    }
});

Instance:

var vm = new Vue(
{
    el: "#main",
    data:
    {
        val1: true,
        val2:
        {
            val3: true
        }
    },
    methods:
    {
        x_toggle: function()
        {
            // VERSION 1
            //this.val1 = !this.val1;
            // VERSION 1
            //this.val2.val3 = !this.val2.val3;
        }
    }
});

VERSION 1’s lines properly hide the template’s <div> but VERSION 2’s don’t. It looks like you have to have the components accept only individual leaf values via v-on in the way described in the current thread’s original post. Am I really mistaken about that? (I also keep hearing about vuex but kind of assumed that my project wouldn’t need much more than one or two object props per component and wouldn’t need to explore beyond ordinary v-on watching.)

I had the same misunderstanding. Apparently, in the "data()" method, you set only the initial value, a static value, with computed, you can “define” dynamic values.

But if you want to update something when props change, you have to listen for them, just like watchers in Angular. In React, when you receive new props, the component just gets updated (if needed).

2 Likes

Uhm, no, that’s not the case. props do update reactively. But If you decide to manually copy them into data once during mounting (as the OP did in “VERSION 2”), that would not get updated as vue obviously can’t track that.

The example discussed is a bit confusing - a working reproduction on jsfiddle tat actually demonstrates the issue would make it much easier to help.

2 Likes

@LinusBorg you are right. I thought you can only bind the “data”, and not the “props”.

This example proves exactly your point:
http://jsfiddle.net/6x2v9y20/11/

Thanks a lot!

One more thing: When you pass an object as a property into a custom component.
and then update only a field inside that object in the component
then the component is NOT rerendered. It think it should be.

[SOLVED]

Vues error messages are just plain simply the best I have EVER seen :slight_smile:

“Vue warn: 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: “color”
(found in component )”

1 Like

This is the solution. I have a prop to a mounted component inside another component that is setting the standard selected option. It did not update the value, until I had an watch for that property inside the custom component that I created.

New to Vue 2 and the forum.

I was pulling my hair out about this recently, trying to get an initial value obtained from a remote data source into an input field that the user could then change.

My particular scenario involved a hierarchy of custom components. The top level component was constructed to contain the data, which was initially null and then obtained from a remote source. The child components were constructed to accept this data via props.

I learned from the docs that I should always bind (v-model) to local data (as opposed to a prop) to avoid the usual “mutating a prop = bad” warning. So I naturally followed the documentation example here:

and created a data() function that set the initial value of the local data to the corresponding props. The end result was empty input fields in my child components. Checking via dev tools I could see that although the prop values were being set correctly in the child components the local data in those components was set to the initial / default value (null or empty string).

The solution was in fact putting a watcher on each prop and doing the local = prop assignment in each function.

I think the documentation, in providing simple examples based on statically defined data, ignores the reality that if the data is dynamically sourced:

  1. the “data()” function assignment technique doesn’t work as intended and will not in fact update the local data subsequent to any change in the prop data.

  2. creating a computed property and binding to that doesn’t help either because these require action of some kind (like user input) to be executed and this scenario occurs before any such input.

The botttom line is a watch function must be assigned to any prop data that is not statically defined.

3 Likes

I agree with adding a real world case that isn’t so static. I pulled my hair out on this one over the last 12-24hrs, and now I’ve come up with a working solution. Your solution sounds very similar, but I’m curious what you would think of how I solved (or if it is, in fact, the same setup): Best Practices: Bootstrapping Ajax data when using Vuex

Forgot about this thread. Amazing how time flies.

All I can say is that the solution I proposed here got very ugly very quickly. The bottom line is putting watchers everywhere actually caused other issues so I had to abandon the concept.

It’s hard to explain what I’ve ultimately done except to say that I have followed the standard (documented) model of how to make data reactive and how to pass data down via props and save to my data store, etc. in response to various events.

The thing to take home is to make sure any data you want to be reactive (watched by Vue) is in the data() method at component construction / initialization time, even if the initial value is null or an empty object / array. Once that is done, Vue pretty much works as advertised. I didn’t quite understand this requirement at first, which is why I had a lot of issues and had to add watchers.

Unfortunately I don’t use Vuex so I can’t really comment on any design that uses it.

Thanks for the reply!

Yeah, the Vuex piece here is what has me stumbling for a solid pattern. With forms, you want to be able to make a mutable copy of some piece of app state (say for a user), so you don’t go changing the data reactively for all the other components that use it while you’re just in “edit mode”.

This gets tricky when the piece of app state you want a mutable copy of can change (because it gets loaded in from a backend server, or updated in some way). You’ve got to keep the copy up to date with respect to the truth (app state), but you just want the darn thing in data so your component can react to changes (think form validation on that user data).

Anyways, thanks for the follow up! All this explanation is really for posterity.

I think this is because javaScript copy problem.
In js, copy primitive type is to copy value, and copy object is to copy reference.
So in data, even if you assign value from props, it just copy values from props,
and does not watch the prop’s change. It’s just initial value.

If you just copy object(ex:this.day_exr), watch does not need.

1 Like

This discussion just saved me hours I am sure. I fixed my issue using a single watch function on my child component.

To be honest, up until now I have had nothing but good feelings about Vue.js, however this feels like a hack.
To summaries the steps required to get this to work for me:

  1. Send the parent data to the child component through a property.
  2. In the data function I use Object.assign({}, propName) to copy the parent data to a local object.
  3. Use v-model in the child to react to form changes.
  4. Add a watch for the property that repeats the copy if the parent data changes.
  5. Use $emit to send updated data back to the parent.

I am glad this conversation was here. Thanks to all involved.