Vue 3 で props を分割代入するとリアクティブが効かない
Vue 3.4 以前では defineProps の戻り値を分割代入するとリアクティブ性が失われ、親の更新が子に反映されません。
Vue 3.5 以降は Reactive Props Destructure が標準で有効になり、分割代入してもリアクティブが保たれます。
#props#reactivity#composition-api#defineProps
公開:
要約
Vue 3 で defineProps の戻り値を分割代入すると、Vue 3.4 以前ではリアクティブが失われ、親コンポーネントの更新が子に伝わりません。Vue 3.5 以降 では Reactive Props Destructure が標準で有効になり、この問題は解消されています。
3.4 以前を使い続ける場合は、分割代入を避けて props.foo の形でアクセスするのが基本です。
よくある原因
- 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 を使います。