Vue 3 の開発中に、次のような警告を見たことはありませんか?
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
これは、props を直接書き換えたときに Vue が出す警告です。
一見「値を更新したいだけ」でも、Vue のリアクティブシステム上は明確な設計違反になります。
この記事では、この警告の本当の意味と、正しい解決パターンを紹介します。
なぜ props を直接変更してはいけないのか
Vue の props は、親コンポーネントから子コンポーネントへの “一方向データフロー” を保証する仕組みです。
親 → 子 に値を渡すのは自由ですが、子 → 親 方向に props を直接変更してしまうと、
親側の状態と Vue のリアクティブシステムの整合性が崩れるため、Vue は警告を出します。
内部的な仕組み
Vue は props を「読み取り専用のリアクティブデータ」として扱います。
親コンポーネントが再レンダリングされると、子コンポーネントの props は再評価されて上書きされます。
そのため、子で props を直接書き換えても、次のレンダリングで親の値で上書きされてしまうのです。
この設計は React の「props are immutable(不変)」と同じ考え方に基づいています。
警告が出る典型的なケース
1. props をそのまま代入して変更している
<script setup>
defineProps({
count: Number
})
function increment() {
// ❌ props を直接変更している
count++
}
</script>
このようなコードは Avoid mutating a prop directly の警告を引き起こします。
解決策 1:ローカルコピーを使う
「props の初期値をもとに、内部で編集したい」というケースでは、
props をコピーしてローカルな state に持つのが最もシンプルです。
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
count: Number
})
const localCount = ref(props.count)
// 親の変更を追従したい場合は watch で同期
watch(() => props.count, (newVal) => {
localCount.value = newVal
})
function increment() {
localCount.value++
}
</script>
<template>
<button @click="increment">{{ localCount }}</button>
</template>
この方法なら、localCount は子コンポーネント専用の状態となり、props を汚染しません。
解決策 2:computed と emit で親子を連携する
「子で操作して親に反映したい」場合は、props を変更する代わりに
イベントを emit して、親のデータを更新するのが Vue の正しい設計です。
<!-- 子コンポーネント ChildCounter.vue -->
<script setup>
const props = defineProps({
count: Number
})
const emit = defineEmits(['update:count'])
function increment() {
emit('update:count', props.count + 1)
}
</script>
<template>
<button @click="increment">{{ props.count }}</button>
</template>
親側では次のようにバインディングします。
<template>
<ChildCounter v-model:count="count" />
</template>
<script setup>
import { ref } from 'vue'
import ChildCounter from './ChildCounter.vue'
const count = ref(0)
</script>
v-model:count 構文により、props.count と emit('update:count') が自動的に連携します。
これが 双方向バインディングを保ちながら一方向データフローを維持する唯一の方法 です。
解決策 3:computed の setter を使う
一部のケースでは、computed の setter を使って emit を簡潔に書けます。
<script setup>
const props = defineProps({ count: Number })
const emit = defineEmits(['update:count'])
const countValue = computed({
get: () => props.count,
set: (val) => emit('update:count', val)
})
</script>
<template>
<input type="number" v-model="countValue" />
</template>
こうすると、v-model バインディングをそのまま使え、
props の直接変更も防げます。
実務での判断基準
| 目的 | 対応方針 |
|---|---|
| props を内部で編集したい | ref にコピーしてローカル状態を管理 |
| 親にも変更を伝えたい | emit (update:propName) で通知 |
| 双方向バインディングを構築したい | v-model / computed setter を利用 |
| props の値を参照するだけ | そのまま利用して OK |
まとめ
「Avoid mutating a prop directly」警告は、Vue のデータフロー設計を破壊しかねない操作に対して発せられる重要なサインです。
ポイント
- props は「親→子」の一方向データのみを意図している
- 子で値を変更したい場合はローカルコピーか emit を使う
v-modelは props と emit の正しい連携パターン- computed の setter を使えばシンプルに記述できる
Vue 3 のリアクティブシステムを正しく理解すれば、
この警告は単なるエラーではなく「設計の指針」として活用できるようになります。

