Vue 3 で「Avoid mutating a prop directly」警告が出る原因と正しい対応

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.countemit('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 のリアクティブシステムを正しく理解すれば、
この警告は単なるエラーではなく「設計の指針」として活用できるようになります。

タイトルとURLをコピーしました