[Vuejs]-Are the props in a Vue component mutable?

2👍

No, never. Vue js communication between parent/children is one way. Props received from parents should NEVER be mutated, otherwise, you will receive a warning if you are working with strict mode enabled.

Data from parents to children must be sent as props. Data from children to parent must be sent within events

The best approach here is to manipulate the data locally in your child component, by setting a local data when the component is mounted, and then emitting an event to the parent when something changes (only if you need to let the parent know that something happened)

props: ['options'],

data: () => ({
    local_options: []
}),

mounted () {
    this.local_options = [ ...this.options]
},

watch: {
   options: function () {
       deep: true,
       handler () {
          this.local_options = [ ...this.options]
       }
   }
},

methods: {
   updateOption(index, value) {
       this.local_options[index].answer = value
       this.$emit('updated', this.local_options)
   },

   removeOption(index) {
       this.local_options.splice(index, 1)
       this.$emit('updated', this.local_options)
   },

   addOption() {
       this.local_options.push({
           answer: '',
           correct: false,
       })
       this.$emit('updated', this.local_options)
   },
},

Then in your parent, where you have your child component, you can do something as per the child event:

<multi-options-component :options="options" @updated="doSomething" />

....
methods: {
    doSomething (options) {
         // Some logic here
    }
}

2👍

I believe that the way @Luciano prefers having a separate local state for the child and manually maintaining its reactivity is a recipe for disaster (not to mention its way overkill considering Vue already has reactivity built-in). I have mentioned this in the comments, I’ll add here again:

  1. If doSomething fails for any reason, then the parent’s options array won’t get updated, whereas local_options array has already been updated within the component. Now parent and local states are out of sync and you have problem.
  2. There is no need to use deep watcher inside every reusable component you build. If you have hundreds of such child components, you’ll have performance hits and you’re unnecessarily consuming a lot of memory creating and maintaining a separate local state for each child component.
  3. You’re losing the reactivity advantage that Vue provides in trying to manually ensure (albeit in an error-prone way) the local state is always in sync with the parent. That’s not how you’re supposed to use Vue.js.

So I’m adding another answer that’s better than my previous answer in terms of reusability and DRY principle, while also being simple and chaos-free:

You need to pass an array back to the parent along with a single custom update event, but instead of creating this array in the mounted lifecycle hook, you create it on the go in each method:

props: ['options'],

methods: {
   updateOption(index, value) {
       let local_options = [ ...this.options]
       local_options[index].answer = value
       this.$emit('update', local_options)
   },

   removeOption(index) {
       let local_options = [ ...this.options]
       local_options.splice(index, 1)
       this.$emit('update', local_options)
   },

   addOption() {
       let local_options = [ ...this.options]
       local_options.push({
           answer: '',
           correct: false,
       })
       this.$emit('update', local_options)
   },
},

Then update your parent component’s option data when the child emits an update event with v-on: directive or its shorthand @

<multi-options :options="options" @update="handleUpdate" />

....
methods: {
    handleUpdate(options) {
         this.options = options; // this reassignment re-renders the child component
    }
}

EDIT: I missed that you’re using v-model on a prop in <b-form-checkbox> element with v-model="option.correct". This will create a two way binding with the prop since it translates to :checked="option.correct" @change="option.correct = $event". Which means you’re mutating the prop inside child component. Do this instead:

            <b-form-checkbox
                :id="`option-${index}`"
                name="options"
                class="f-14 text-muted ml-1"
                :checked="option.correct"
                @change="setOptionsCorrect(index, $event)"
            >
                Correct?
            </b-form-checkbox>

add the method under your methods:

setOptionsCorrect(index, value) {
    let local_options = [ ...this.options]
    local_options[index].correct = value
    this.$emit('update', local_options)
}

EDIT 2: If you insist on using v-model for style preferences, you could have a computed property derive from the options. But you’ll need a separate setter that emits the event to parent, otherwise you’ll run into the same "mutating props" problem

computed : {
    computedOptions: {
        get: function() {
            return this.options
        },
        set: function(newOptions) {
            this.$emit("update", newOptions)
        }
    }
},
methods: {
   updateOption(index, value) {
       let local_options = [ ...this.options]
       local_options[index].answer = value
       this.options = local_options
   },

   removeOption(index) {
       let local_options = [ ...this.options]
       local_options.splice(index, 1)
       this.options = local_options
   },

   addOption() {
       let local_options = [ ...this.options]
       local_options.push({
           answer: '',
           correct: false,
       })
       this.options = local_options
   },
}
👤Mythos

0👍

You’re not supposed to mutate props from the component itself. From docs, this is to prevent child component from accidentally mutating parent components state, which can make your app’s data flow harder to understand.

What you can do instead is emit custom events from child component:

updateOption(index, value) {
    this.$emit("myUpdateEvent", { index, value });
}
removeOption(index) {
    this.$emit("myRemoveEvent", { index });
}
addOption() {
    this.$emit("myAddEvent");
}

Then listen to them in the parent component and make changes to the original array that you’re passing to the child component as props.

<multi-options @myAddEvent="handleAddEvent()" @myUpdateEvent="handleUpdateEvent($event.index, $event.value)" @myRemoveEvent="handleRemoveEvent($event.index)" :options="myOptions"/>
data() {
    return {
        myOptions: [],
    }
},
methods: {
    handleUpdateEvent(index, value) {
        this.myOptions[index].answer = value
    },
    handleRemoveEvent(index) {
        this.myOptions.splice(index, 1);
    },
    handleAddEvent() {
        this.myOptions.push({
                answer: '',
                correct: false,
            });
    }
}
👤Mythos

Leave a comment