Why is this an issue?

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.

Code examples

Noncompliant code example

<!-- 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>

Compliant solution

<!-- 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>

Noncompliant code example

<!-- 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>

Compliant solution

<!-- 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>

Resources

Documentation

Related rules