I am trying to update the child component value from the parent component, when I set the value in the prop, I do get the following error,
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.
I tried the below solution found on the internet but by doing so my child component prop is not getting changed when I update the parent component
Parent component
<template>
<child :index-val="indexVal"> </child>
</template>
<script>
export default {
props: [indexVal],
data() {
return {
indexVal: 0,
}
},
methods: {
updateVal() {
// updating value from parent
this.indexVal = 2
},
},
}
</script>
Child component
<template>
<div>
<!-- Tabs with card integration -->
<b-card no-body>
<b-tabs v-model="tabIndex" small card>
<b-tab title="tab1">tab1</b-tab>
<b-tab title="tab2">tab2</b-tab>
</b-tabs>
</b-card>
</div>
</template>
<script>
export default {
props: [indexVal],
data() {
return {
tabIndex: this.indexVal,
}
},
}
</script>
CodePudding user response:
The error you're facing happens when you're trying to mutate the value of a prop
, because prop
values are coming from parents. If the parent changes it, your change inside the child will be overwritten.
Thus, when a child needs to change a prop
, it has to emit an event to the parent, update the parent value which, in turn flows the change back into the child, via the prop.
Props down, events up!
To eliminate part of the boilerplate for this mechanism, Vue 2 provided the .sync
modifier.
In Vue3, .sync
was deprecated and removed (breaking change)!
someProp.sync
has been replaced by v-model:someProp
, because:
- they were providing the same functionality (dual binding),
- this increased the confusion between the two syntaxes and their particular implementation details.
- now
v-model
behaviour is aligned across native form elements and Vue components.
Vue's author (Evan You), has declared over the past years that, going forward, the core team's intention is to reduce the API surface, by removing unnecessary or confusing syntax options.
Vue 2 example:
Vue.component('tab', {
template: `#tab-tpl`,
props: ['index', 'text'],
computed: {
tIndex: {
get() { return this.index },
set(val) { this.$emit('update:index', val); }
}
}
})
new Vue({
el: '#app',
data: () => ({
tabs: [{text: 'one'}, {text: 'two'}, {text: 'three'}],
tabIndex: 0
}),
computed: {
currentIndex: {
get() { return this.tabIndex },
set(val) {
this.tabIndex = (this.tabs.length val) % this.tabs.length;
}
}
}
})
.tab-pills {
display: flex;
border-bottom: 1px solid #ccc;
cursor: pointer;
}
.active {
color: red;
}
.tab-pills > div { padding: .3rem .7rem; }
.tab-content { padding: 1rem .7rem; }
.tab-body { padding: 1rem 0 }
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<div >
<div v-for="(tab, key) in tabs"
v-text="tab.text"
:
:key="key"
@click="currentIndex = key">
</div>
</div>
<tab :index.sync="currentIndex" :text="tabs[currentIndex].text" ></tab>
</div>
<template id="tab-tpl">
<div>
<div v-text="text" ></div>
<button @click="tIndex -= 1">Prev</button>
<button @click="tIndex = 1">Next</button>
</div>
</template>
Same thing, in Vue 3 (Composition API):
const { defineComponent, toRefs, reactive, computed, createApp } = Vue;
const Tab = defineComponent({
template: '#tab-tpl',
props: ['index', 'text'],
setup: (props, { emit }) => ({
tIndex: computed({
get: () => props.index,
set: val => emit('update:index', val)
})
})
});
const Tabs = defineComponent({
template: '#tabs-tpl',
components: { Tab },
setup() {
const state = reactive({
tabs: ['one', 'two', 'three'].map(text => ({ text })),
tabIndex: 0,
currentIndex: computed({
get: () => state.tabIndex,
set: val => state.tabIndex = (state.tabs.length val) % state.tabs.length
})
})
return {...toRefs(state)}
}
});
createApp(Tabs).mount('#app')
.tab-pills {
display: flex;
border-bottom: 1px solid #ccc;
cursor: pointer;
}
.active {
color: red;
}
.tab-pills > div { padding: .3rem .7rem; }
.tab-content { padding: 1rem .7rem; }
.tab-body { padding: 1rem 0 }
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
<div id="app"></div>
<template id="tabs-tpl">
<div >
<div v-for="(tab, key) in tabs"
v-text="tab.text"
:
:key="key"
@click="currentIndex = key">
</div>
</div>
<tab v-model:index="currentIndex"
:text="tabs[currentIndex].text"
></tab>
</template>
<template id="tab-tpl">
<div v-text="text" ></div>
<button @click="tIndex -= 1">Prev</button>
<button @click="tIndex = 1">Next</button>
</template>
In both examples, the tab controls at the top change tabIndex
at parent level.
The buttons inside the tab, change it from the child (by emitting the updated value to parent).
The important part is that tabIndex
changes in parent component, which drives the update in the child. Don't mind the currentIndex
computed in parent, that's just an implementation detail (it allows going to first tab when clicking Next
on last tab and going to last when hitting Prev
on first tab - that's the only reason why I added it, it's not necessary for all of this to work).
If something is unclear, check the doc links I posted above, for the Vue version you're using.