148

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?

mhrabiee
  • 805
  • 10
  • 23
Dominik Serafin
  • 3,616
  • 3
  • 17
  • 27
  • 27
    This is the most confusing thing about Vue in my opinion: Every `data` is `two-way` bound, but you can't pass `data` to components, you pass `props`, but you can't change the received `props` nor convert the `props` to `data`. Then what? One thing that I learned is that you should pass `props` down and trigger events up. That is, if the component wants to change the `props` it received, it should call an event and be "rerendered". But then you're left with a `one-way` binding exactly like React and I don't see the use for `data` then. Pretty confusing. – Andre Pena Nov 03 '16 at 17:56
  • 2
    Data is primarily intended for the private use of the component. Everything placed on it within the context of the component is reactive and can be bound to. The concept with props is to pass values into a component but keep the component from being able to silently introduce state changes in the parent by changing a passed value. It's better to make it explicit in an event as you indicated. This was a philosophy change from Vue 1.0 to 2.0. – David K. Hess Nov 03 '16 at 19:37
  • Today I've tried to start a thread over here: https://forum.vuejs.org/t/arbitrary-nested-dynamic-forms-still-unclear-mutating-object-props-bad-or-good-ok/55673 – bgraves Feb 06 '19 at 14:36
  • 1
    `data` is state, `props` are arguments, and `events` bubble up. You can dress up a UI framework anyway you want, but those three things still must be present and work as they always have. I have never encountered a UI that doesn't fundamentally operate the same way under the hood. – Jamie Marshall Mar 02 '21 at 00:39

6 Answers6

160

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://v2.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 How do I correctly clone a JavaScript object?.

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

Maxim Mazurok
  • 3,856
  • 2
  • 22
  • 37
Dominik Serafin
  • 3,616
  • 3
  • 17
  • 27
  • 1
    so it is ok practice to have changes propagated to parent component? – igor Apr 04 '18 at 21:12
  • 1
    @igor personally I'd try to avoid it as much as you can. It will make your components unpredictable. I think better way to get changes in parent component is to use "v-model" method where you $emit changes from your child component to parent component. https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components – Dominik Serafin Apr 05 '18 at 14:27
  • yes but what about deep objects? should I deep copy? should I redesign my components to not use deep object? ie a prop with: `{ ok: 1, notOk: { meh: 2 } }` if set to an internal data with any of the previous cloning method would still change the prop `notOk.meh` – Nikso Oct 04 '18 at 04:34
  • @Nikso yeah I think you can either redesign your component to use shallow objects or deep clone them using e.g. JSON stringify/parse method when possible, Lodash cloneDeep or any other deep copy method preferred by you. – Dominik Serafin Oct 05 '18 at 08:40
  • Today I've tried to start a thread over here: https://forum.vuejs.org/t/arbitrary-nested-dynamic-forms-still-unclear-mutating-object-props-bad-or-good-ok/55673 – bgraves Feb 06 '19 at 14:37
  • 2
    Thanks for the excellent question and answer @DominikSerafin. This perfectly illustrates the achilles heal of Vue.js. As a framework it handles componentization so well, but passing data amongst the comps is a major PITA. Just look at the nomenclature nightmare created. So now I need to prefix all my props with `initial` just to be able to use them within my comp. It would be so much cleaner if we could just pass a prop called `data` to any comp and have it automagically propagate the localized data within without all this `initialPropName` madness. – AJB Sep 16 '19 at 08:55
  • Thank you @AJB. Personally I don't find I need to pass data between parent/child components very frequently. And I think this should be avoided whenever possible. Maybe there are some parts in your architecture that could be refactored using different approach? E.g. using Vuex or v-model? – Dominik Serafin Sep 16 '19 at 09:24
  • 1
    @DominikSerafin I hear what you're saying, and you're not wrong. But write a form-builder app using Vue.js and you'll quickly see what I mean. I'm actually in the process of changing my technique of sharing data from `prop --> comp.data` to using Vue-Stash for *all* data (or Vuex would work too). But now I'm just bouncing my data off the `window` object as I used to do before the "modern" JS frameworks imposed their opinion on me. So it begs the question: What's the point of the comp.data property at all? – AJB Sep 16 '19 at 09:32
  • @AJB, have you looked into v-model? This has been really useful for me for any form/input related components https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components – Dominik Serafin Sep 16 '19 at 20:44
  • For method 1, v-model has to do with the parent component. Here we are talking about the design of the child component. Even if you use v-model on the parent component, you will still need to emit back up any changes to the counter variable. – samlaf Aug 21 '21 at 17:38
  • Just be aware, if you have a nested object, you need to do a deep copy. data() { return { copiedThing: { ...this.thing, nestedThing: { ...this.thing.nestedThing } }, – LMS5400 Nov 03 '21 at 20:32
43

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/vue@2.5.17/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/

Anbuselvan Rocky
  • 606
  • 6
  • 22
Igor Parra
  • 10,214
  • 10
  • 69
  • 101
  • Hey @igor-parra, it seems like you've submited duplicated answers. And also it might be good to mention that spread operator isn't supported in all browsers and might need transpiling step for better compatibility. Otherwise it looks great - thanks for adding this alternative method! – Dominik Serafin Aug 20 '18 at 09:20
  • @dominik-serafin Yes, you right, I added a reference to ES6. In webpack based applications it is so easy to have preconfigured babel to make full use of modern javascript. – Igor Parra Aug 20 '18 at 21:38
  • `recordLocal: [...this.record]` (in my case record is an array) did not work for me - after loading and inspecting the vue component, `record` contains the items and `recordLocal` was an empty array. – Christian Jun 30 '20 at 19:56
  • When I use it though COMPUTED property it goes well, for me when I try to set inside the data it is not working =(... But I got it using `computed` – felipe muner Jul 17 '20 at 06:26
  • 2
    Be carful. The spread operator only shallow clones, so for objects that contain objects or arrays you will still copy pointers instead of getting a new copy. I use cloneDeep from lodash. – Cindy Conway Jul 17 '20 at 17:33
17

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

jonan.pineda
  • 1,264
  • 11
  • 19
  • 4
    For pass-by-value props that is fine. In this case it's an object, so changing it will affect the parent. From that page: "Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state." – Jake Oct 01 '18 at 00:56
  • This works for me and is clearly documented. this line in the answer above conflicts with vue documentation as far as I can tell "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". " – Nathaniel Rink Apr 26 '19 at 17:32
  • 3
    So after 4 years and hundreds of hours of learning curve I'm right back where I started just bouncing vars off the `window` object. – AJB Sep 16 '19 at 09:21
7

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;
      }
    },
  },
murphy1312
  • 553
  • 9
  • 18
2

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.

Moj
  • 358
  • 2
  • 8
  • Surprise, surprise, now when you emit the event your exposing internal object reference to parent ;) Now when you do anything with it that would trigger change in children. – Kamil Latosinski May 22 '23 at 12:01
0

Following up on Cindy's comment on another answer:

Be carful. The spread operator only shallow clones, so for objects that contain objects or arrays you will still copy pointers instead of getting a new copy.

Indeed this is the case. Changes within objects inside arrays will still propagate to your components even when a spread operator is employed.

Here was my solution (using Composition API):

setup() {
    properties = ref([])

    onMounted(() => {
      properties.value = props.initialProperties.map((obj) => ({ ...obj }));
    })
}

This worked to set the values and prevent them from getting changed, even if the data was changed in the parent component.

Adam Starrh
  • 6,428
  • 8
  • 50
  • 89