Vue 3 で props を分割代入するとリアクティブが効かない
Vue 3.4 以前では defineProps の戻り値を分割代入するとリアクティブ性が失われ、親の更新が子に反映されません。
Vue 3.5 以降は Reactive Props Destructure が標準で有効になり、分割代入してもリアクティブが保たれます。
公開: 更新:
要約
Vue 3 で defineProps の戻り値を分割代入すると、Vue 3.4 以前ではリアクティブが失われ、親コンポーネントの更新が子に伝わりません。Vue 3.5 以降 では Reactive Props Destructure が標準で有効になり、この問題は解消されています。
3.4 以前を使い続ける場合は、分割代入を避けて props.foo の形でアクセスするのが基本です。
実行例
Vue 3.4.38 で defineProps の戻り値を分割代入し、computed の中で参照すると、親の foo を world に変えても子の表示は HELLO のままだった。props.foo で参照すれば WORLD に更新され、Vue を 3.5.43 に上げると分割代入のままでも更新が届いている。
$ npm ls vue --depth=0
app@1.0.0 /tmp/tmp.6KPyC2F3uY/app
`-- vue@3.4.38$ node run.mjs Child.vue
parent foo = hello, child shows HELLO
parent foo = world, child shows HELLO$ node run.mjs ChildProps.vue
parent foo = hello, child shows HELLO
parent foo = world, child shows WORLD$ npm install --silent --no-audit --no-fund vue@3.5$ npm ls vue --depth=0
app@1.0.0 /tmp/tmp.6KPyC2F3uY/app
`-- vue@3.5.43$ node run.mjs Child.vue
parent foo = hello, child shows HELLO
parent foo = world, child shows WORLD— 2026-10-08 時点の出力
検証環境
- 検証日
- 実行環境
node:20Debian GNU/Linux 12 (bookworm)- バージョン
- Node.js 20.20.2
- npm 10.8.2
- Python 3.11.2
- Git 2.39.5
- Vue 3.5.43
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- Vue 3.4 以前で
const { foo } = defineProps<{ foo: string }>()と分割代入しfooを直接参照している - computed や watch の中で分割代入後の変数を参照しており、依存追跡から外れている
reactive({ ...props })やref(props.foo)で包んでしまい、元の props と切り離されている- デフォルト値の指定に
withDefaultsを使っておらず、undefined のまま渡っている
解決策
1. Vue 3.5 以降にアップグレードする
Vue 3.5 で Reactive Props Destructure(新しいタブで開く) が安定版になり、分割代入してもリアクティブが保たれます。
<script setup lang="ts">
import { computed, watch } from "vue";
const { foo, count = 0 } = defineProps<{
foo: string;
count?: number;
}>();
watch(() => foo, (v) => console.log(v));
const upper = computed(() => foo.toUpperCase());
</script>デフォルト値も = で直接書け、3.4 以前で必要だった withDefaults は不要になります。
2. Vue 3.4 以前は props 経由でアクセスする
defineProps の戻り値を props として受け取り、props.foo の形で参照します。
<script setup lang="ts">
import { computed, watch } from "vue";
const props = defineProps<{ foo: string }>();
watch(() => props.foo, (v) => console.log(v));
const upper = computed(() => props.foo.toUpperCase());
</script>props.foo は内部的に getter として動作し、参照のたびに最新値を取り出すためリアクティブが維持されます。
3. withDefaults でデフォルト値を指定する(3.4 以前)
<script setup lang="ts">
const props = withDefaults(defineProps<{ foo?: string }>(), {
foo: "default",
});
</script>4. props を reactive / ref でラップしない
// NG: props と切り離されてしまう
const localProps = reactive({ ...props });reactive({ ...props }) は分割代入と同じくスナップショットになり、親の更新が伝わりません。
同期したいなら watchEffect で都度コピーするか、Vue 3.5 の Reactive Props Destructure を使います。