【Vue3】オブジェクト/配列のpropsは直接参照を避けよう
はじめに
Vue3におけるオブジェクト/配列のprops
に関する記事です。
与件
- Vue3, Nuxt3, TypeScript
-
props
でオブジェクト/配列を親コンポーネントから受け取る
propsの仕様
Vue3において、オブジェクトや配列をprops
として渡した場合、子コンポーネントがそのオブジェクトや配列自体を変更することはできませんが、オブジェクトや配列のネストされたプロパティを変更することはできてしまいます。
これは、JavaScriptでオブジェクトや配列が参照渡しであることに起因しているようです。
単一の値をpropsとして渡す場合
単一の値をprops
として渡す場合、子コンポーネントでprops
の値を変更すると、その変更は親コンポーネントに反映されません。これは、props
が単一の値であるため、子コンポーネントでの変更が単なるローカルな変数の変更と同じ扱いになるためです。
<script setup lang="ts">
const props = defineProps(['hoge'])
// 警告:propsは読み取り専用です!になる
props.hoge = 'bar'
</script>
オブジェクトや配列をpropsとして渡す場合
オブジェクトや配列をprops
として渡した場合、子コンポーネントが親コンポーネントから渡されたオブジェクトや配列のプロパティを直接変更することはできません。ただし、オブジェクトや配列のプロパティが変更された場合、Vue3はその変更を検知して、親コンポーネントのデータも更新されます。
<script setup lang="ts">
interface MyObject {
name: string;
age: number;
hobbies: string[];
}
const props = defineProps({
myObject: {
type: Object as () => MyObject,
required: true,
},
});
const changeHobbies = () => {
// 親からpropsで受け取ったmyObjectのプロパティを変更できてしまう
props.myObject.hobbies.push('baking');
};
</script>
<template>
<div>
<p>{{ myObject.name }}</p>
<p>{{ myObject.age }}</p>
<ul>
<li v-for="hobby in myObject.hobbies" :key="hobby">{{ hobby }}</li>
</ul>
<button @click="changeHobbies">Change Hobbies</button>
</div>
</template>
起こりうる問題
こうした仕様は、子コンポーネントが親の状態に影響を与えることを許してしまいます...
以下の公式ドキュメントでも、後からデータの流れを見極めるのが難しくなるため、親と子を密に結合させる設計でない限り、このような変更を避けることがベストプラクティスであると記載されてあります。
意図しないデータフローを避けてオブジェクト/配列をpropsとして渡す方法
ダメな例
何も考えずにオブジェクトや配列を渡すとこんな感じでしょうか。
<script setup lang="ts">
interface HogeObject {
name: string;
age: number;
}
const props = defineProps({
arrHoge: {
type: Array as () => Array<number | string>,
required: true,
},
objHoge: {
type: Object as () => HogeObject,
required: true,
},
});
const arrHoge = ref<Array<number | string>>(props.arrHoge);
const objHoge = ref<HogeObject>(props.objHoge);
</script>
<template>
<div>
<p>Array: {{ arrHoge }}</p>
<p>Object: {{ objHoge }}</p>
</div>
</template>
このコードでは、props.arrHoge
を直接参照しています。props.arrHoge
と新しい配列の両方が同じメモリ位置を参照しているため、props.arrHoge
内の変更が新しい配列に反映され、親コンポーネントの値に影響を与える可能性があります。
ベストプラクティス
<script setup lang="ts">
import { defineProps, ref, Ref } from 'vue';
interface HogeObject {
name: string;
age: number;
}
const props = defineProps({
arrHoge: {
type: Array as () => Array<number | string>,
required: true,
},
objHoge: {
type: Object as () => HogeObject,
required: true,
},
});
// スプレッド構文で新しい配列として展開
const arrHoge: Ref<Array<number | string>> = computed(() => [...props.arrHoge]);
const objHoge: Ref<HogeObject> = computed(() => ({ ...props.objHoge }));
</script>
<template>
<div>
<p>Array: {{ arrHoge }}</p>
<p>Object: {{ objHoge }}</p>
</div>
</template>
このコードではarrHoge
とobjHoge
を新しい配列と新しいオブジェクトとして計算された値として定義しています。こうすることで、props.arrHoge
とprops.objHoge
の値が変更されても、arrHoge
とobjHoge
は常に新しい配列と新しいオブジェクトを参照するようになります。
ちなみにめちゃくちゃ省略して
<script setup lang="ts">
ref<Array<number | string>>([...props.arrHoge])
</script>
とかでも良いんですが、これだとpropsの値が変更された場合に手動で value
プロパティを設定する必要があるので場合によりますね😅
まとめ
- オブジェクトや配列を
props
として渡した場合、子コンポーネントがprops
のバインディングを変更することはできないが、オブジェクトや配列のネストされたプロパティを変更することはできてしまう。 - 意図しないデータフローを防ぐため、オブジェクト/配列の
props
は直接参照しないようにする - オブジェクト/配列の
props
は新しいオブジェクト/配列として別々のメモリ位置を参照するようにする
参考資料
Discussion