できない.dev

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 の形でアクセスするのが基本です。

よくある原因

  1. Vue 3.4 以前で const { foo } = defineProps<{ foo: string }>() と分割代入し foo を直接参照している
  2. computed や watch の中で分割代入後の変数を参照しており、依存追跡から外れている
  3. reactive({ ...props })ref(props.foo) で包んでしまい、元の props と切り離されている
  4. デフォルト値の指定に 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 を使います。

この記事は役立ちましたか?