Vue enforces a one-way data flow principle: data is passed from parent to child components through props, and changes should be communicated back to the parent by emitting events. Directly mutating a prop breaks this contract.
Directly reassigning a prop triggers a Vue readonly warning in development mode. Mutating a prop’s contents in place, for example by calling
push() on a prop array, is more insidious: Vue does not warn about it, yet the parent component’s state changes as a side effect because
both parent and child share the same reference. In either case, the modification makes the data flow implicit and leads to bugs that are difficult to
reproduce and debug.
To fix the issue, declare the event the child raises (with emits or defineEmits), call
$emit()/emit() instead of mutating the prop, and have the parent register a listener that updates its own state.
Alternatively, create a local copy of the prop when a local modification does not need to reach the parent.
<!-- Parent.vue -->
<template>
<ItemList :items="items" />
</template>
<script>
import ItemList from './ItemList.vue';
export default {
components: { ItemList },
data() {
return {
items: []
};
}
}
</script>
<!-- ItemList.vue -->
<script>
export default {
props: ['items'],
methods: {
add(item) {
this.items.push(item); // Noncompliant: mutating a prop reference
},
reset() {
this.items = []; // Noncompliant: reassigning a prop
}
}
}
</script>
<!-- Parent.vue -->
<template>
<ItemList :items="items" @update:items="items = $event" /> <!-- Registers a listener for the event emitted by ItemList.vue -->
</template>
<script>
import ItemList from './ItemList.vue';
export default {
components: { ItemList },
data() {
return {
items: []
};
}
}
</script>
<!-- ItemList.vue -->
<script>
export default {
props: ['items'],
emits: ['update:items'],
methods: {
add(item) {
this.$emit('update:items', [...this.items, item]);
},
reset() {
this.$emit('update:items', []);
}
}
}
</script>
<!-- Parent.vue -->
<template>
<ItemList :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ItemList from './ItemList.vue';
const items = ref<string[]>([]);
</script>
<!-- ItemList.vue -->
<script setup lang="ts">
const props = defineProps<{ items: string[] }>();
function add(item: string) {
props.items.push(item); // Noncompliant: mutating a prop reference
}
function reset() {
props.items = []; // Noncompliant: reassigning a prop
}
</script>
<!-- Parent.vue -->
<template>
<ItemList :items="items" @update:items="items = $event" /> <!-- Registers a listener for the event emitted by ItemList.vue -->
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ItemList from './ItemList.vue';
const items = ref<string[]>([]);
</script>
<!-- ItemList.vue -->
<script setup lang="ts">
const props = defineProps<{ items: string[] }>();
const emit = defineEmits<{ 'update:items': [value: string[]] }>();
function add(item: string) {
emit('update:items', [...props.items, item]);
}
function reset() {
emit('update:items', []);
}
</script>