What's the correct way to pass props as initial data in Vue.js 2?

Javascriptvue.jsVuejs2

Javascript Problem Overview


So I want to pass props to an Vue component, but I expect these props to change in future from inside that component e.g. when I update that Vue component from inside using AJAX. So they are only for initialization of component.

My cars-list Vue component element where I pass props with initial properties to single-car:

// cars-list.vue

<script>
    export default {
        data: function() {
            return {
                cars: [
                    {
                        color: 'red',
                        maxSpeed: 200,
                    },
                    {
                        color: 'blue',
                        maxSpeed: 195,
                    },
                ]
            }
        },
    }
</script>

<template>
    <div>
        <template v-for="car in cars">
            <single-car :initial-properties="car"></single-car>
        </template>
    </div>
</template>

The way I do it right now it that inside my single-car component I'm assigning this.initialProperties to my this.data.properties on created() initialization hook. And it works and is reactive.

// single-car.vue

<script>
    export default {
        data: function() {
            return {
                properties: {},
            }
        },
        created: function(){
            this.data.properties = this.initialProperties;
        },
    }
</script>

<template>
    <div>Car is in {{properties.color}} and has a max speed of {{properties.maxSpeed}}</div>
</template>

But my problem with that is that I don't know if that's a correct way to do it? Won't it cause me some troubles along the road? Or is there a better way to do it?

Javascript Solutions


Solution 1 - Javascript

Thanks to this https://github.com/vuejs/vuejs.org/pull/567 I know the answer now.

Method 1

Pass initial prop directly to the data. Like the example in updated docs:

props: ['initialCounter'],
data: function () {
    return {
        counter: this.initialCounter
    }
}

But have in mind if the passed prop is an object or array that is used in the parent component state any modification to that prop will result in the change in that parent component state.

Warning: this method is not recommended. It will make your components unpredictable. If you need to set parent data from child components either use state management like Vuex or use "v-model". https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Method 2

If your initial prop is an object or array and if you don't want changes in children state propagate to parent state then just use e.g. Vue.util.extend [1] to make a copy of the props instead pointing it directly to children data, like this:

props: ['initialCounter'],
data: function () {
    return {
        counter: Vue.util.extend({}, this.initialCounter)
    }
}

Method 3

You can also use spread operator to clone the props. More details in the Igor answer: https://stackoverflow.com/a/51911118/3143704

But have in mind that spread operators are not supported in older browsers and for better compatibility you'll need to transpile the code e.g. using babel.

Footnotes

[1] Have in mind this is an internal Vue utility and it may change with new versions. You might want to use other methods to copy that prop, see "https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object";.

My fiddle where I was testing it: https://jsfiddle.net/sm4kx7p9/3/

Solution 2 - Javascript

In companion to @dominik-serafin's answer:

In case you are passing an object, you can easily clone it using spread operator(ES6 Syntax):

 props: {
   record: {
     type: Object,
     required: true
   }
 },

data () { // opt. 1
  return {
    recordLocal: {...this.record}
  }
},

computed: { // opt. 2
  recordLocal () {
    return {...this.record}
  }
},

But the most important is to remember to use opt. 2 in case you are passing a computed value, or more than that an asynchronous value. Otherwise the local value will not update.

Demo:

Vue.component('card', { 
  template: '#app2',
  props: {
    test1: null,
    test2: null
  },
  data () { // opt. 1
    return {
      test1AsData: {...this.test1}
    }
  },
  computed: { // opt. 2
    test2AsComputed () {
      return {...this.test2}
    }
  }
})

new Vue({
  el: "#app1",
  data () {
    return {
      test1: {1: 'will not update'},
      test2: {2: 'will update after 1 second'}
    }
  },
  mounted () {
    setTimeout(() => {
      this.test1 = {1: 'updated!'}
      this.test2 = {2: 'updated!'}
    }, 1000)
  }
})

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

<div id="app1">
  <card :test1="test1" :test2="test2"></card>
</div>

<template id="app2">
  <div>
    test1 as data: {{test1AsData}}
    <hr />
    test2 as computed: {{test2AsComputed}}
  </div>
</template>

https://jsfiddle.net/nomikos3/eywraw8t/281070/

Solution 3 - Javascript

I believe you are doing it right because it is what's stated in the docs.

> Define a local data property that uses the prop’s initial value as its initial value

https://vuejs.org/guide/components.html#One-Way-Data-Flow

Solution 4 - Javascript

Second or third time I run into that problem coming back to an old vue project.

Not sure why it is so complicated in vue, but it can we done via watch:

export default {

  props: ["username"],

  data () {
    return {
      usernameForLabel: "",
    }
  },

  watch: {
    username: {
      immediate: true,
      handler (newVal, oldVal) {
        this.usernameForLabel = newVal;
      }
    },
  },

Solution 5 - Javascript

Just as another approach, I did it through watchers in the child component.

This way is useful, specially when you're passing an asynchronous value, and in your child component you want to bind the passed value to v-model.

Also, to make it reactive, I emit the local value to the parent in another watcher.

Example:

  data() {
    return {
      properties: {},
    };
  },
  props: {
    initial-properties: {
      type: Object,
      default: {},
    },
  },
  watch: {
    initial-properties: function(newVal) {
      this.properties = {...newVal};
    },
    properties: function(newVal) {
      this.$emit('propertiesUpdated', newVal);
    },
  },

This way I have more control and also less unexpected behaviour. For example, when props that passed by the parent is asynchronous, it may not be available at the time of created or mounted lifecycle. So you can use computed property as @Igor-Parra mentioned, or watch the prop and then emit it.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDominik SerafinView Question on Stackoverflow
Solution 1 - JavascriptDominik SerafinView Answer on Stackoverflow
Solution 2 - JavascriptIgor ParraView Answer on Stackoverflow
Solution 3 - Javascriptjonan.pinedaView Answer on Stackoverflow
Solution 4 - Javascriptmurphy1312View Answer on Stackoverflow
Solution 5 - JavascriptMojView Answer on Stackoverflow