Vue 3(およびNuxt 3)を使用していると、特に配列やオブジェクトを扱う場合、propsが原因で予期しない動作に遭遇することがあります。この問題は、JavaScriptの参照渡しの特性とVueのリアクティブシステムが組み合わさることで発生し、親コンポーネントの状態に影響を及ぼす可能性があります。この記事では、なぜpropsを変更することがアンチパターンとされるのか、その問題が発生し得るシナリオ、そして適切な対処法を解説します。
なぜPropsの変更がアンチパターンなのか?
VueにおけるPropsの役割
props
は親コンポーネントから子コンポーネントにデータを渡すための仕組みであり、読み取り専用として設計されています。これにより、一方向のデータフローが維持されます。props
を直接変更することは、この原則を破り、以下のような問題を引き起こします:
-
予期しない副作用
子コンポーネントでprops
を変更すると、親コンポーネントの状態にも影響を与え、バグの原因になります。 -
予測不可能な挙動
Vueのリアクティブシステムはprops
が不変であると仮定しています。この仮定を破ると、予測不可能な動作につながります。 -
開発モードでの警告
Vue 3では、props
を直接変更しようとすると警告が表示されます。
Propsの静かな変更問題:Vueが警告を出さない場合
通常、Vueはprops
の直接変更を警告します。しかし、以下のようなケースではこの保護機能が回避されることがあります。
const selectedItems = ref(props.selected); // propsをrefに代入
selectedItems.value.splice(index, 1); // 配列を破壊的に変更
なぜこの問題が発生するのか?
-
JavaScriptの参照渡しの特性
配列であるprops.selected
をref
に代入すると、ref
とprops
はメモリ内で同じ参照を共有します。そのため、ref
のvalue
を変更すると、元のprops
も変更されます。 -
Vueにおけるインスタンスの再生成
親コンポーネントがprops
を更新すると、Vueのリアクティブシステムは新しいインスタンスを生成してその値を渡します。この結果、以前の参照が切れるため、ref
が持つ値は親の最新の状態を反映しなくなります。この挙動により、開発者が意図しない一貫性の欠如が発生することがあります。 -
ref
は初期値として動作
ref(props.selected)
は、props.selected
の現在の値を初期値としてref
に設定するだけであり、props.selected
そのものをリアクティブにラップするわけではありません。このため、props.selected
が後から更新されてもref
はそれに追随しません。 -
警告が出ない理由
この変更はref
を介して間接的に行われるため、Vueはprops
が直接変更されたとは認識せず、警告を出しません。
この問題が発生するシナリオ
-
ref
やreactive
でPropsをラップする場合:const selectedItems = ref(props.selected); // 同じ参照を保持 selectedItems.value.push(newItem); // `selectedItems`と`props.selected`両方が変更される
-
オブジェクトや配列の参照渡し:
配列やオブジェクトとして渡された
props
は、明示的にコピーしない限り、参照が共有されます。 -
破壊的な配列操作:
push
、splice
、sort
などのメソッドは、配列を直接変更し、ref
と親のprops
の両方に影響を与えます。
対策:Propsを安全に扱う方法
props
を直接または間接的に変更しないために、データのコピー、リアクティブな参照の利用、不変性の保証といった方法があります。それぞれのベストプラクティスを順番に見ていきましょう。
特に、props
の更新を反映させる場合はtoRef
が効果的です。また、変更不要なデータにはcomputed
を活用する方法が有効です。それぞれの状況に適した対策を順番に見ていきましょう。
1. 浅いコピーを作成
コンポーネント内で使用する前に、props
のコピーを作成します。
const selectedItems = ref([...props.selected]); // 浅いコピー
ネストされたオブジェクトや配列の場合は、深いコピーを使用します:
const selectedItems = ref(JSON.parse(JSON.stringify(props.selected))); // 深いコピー
2. structuredClone
を使用
最新のJavaScriptでは、structuredClone
を使用して効率的な深いコピーが可能です。
const selectedItems = ref(structuredClone(props.selected));
これはDate
オブジェクトやTypedArray
のような複雑な構造も処理できます。
3. 不変の派生データにcomputed
を使用
変更が必要ない場合、computed
プロパティを使用して派生データを扱います。
import { computed } from 'vue';
const selectedItems = computed(() => [...props.selected]);
4. toRef
を使用してリアクティブにする
もしprops
の更新を反映させたい場合には、toRef
を使用することで、props.selected
をリアクティブな参照として扱えます。
import { toRef } from 'vue';
const selectedItems = toRef(props, 'selected');
この方法では、親コンポーネントでprops.selected
が変更されても、子コンポーネントのselectedItems
が自動的に更新されます。
5. Vueのreadonly
ユーティリティを活用
props
をreadonly
でラップして、不変性を保証します。
import { readonly } from 'vue';
const selectedItems = ref(readonly(props.selected));
これにより、selectedItems.value
を変更しようとするとエラーが発生します。
6. Propsを直接ref
にラップしない
ref
でラップする代わりに、ローカル状態を別途用意し、コピーを初期化します。
const selectedItems = ref(props.selected ? [...props.selected] : []);
Nuxt 4ではどうなるのか?
現在のところ、Nuxt 4自体にこの問題を直接解決するような変更はありません。ただし、NuxtはVueをベースに構築されているため、将来的にVueのリアクティブシステムに改善や変更が加えられれば、それがNuxtにも反映されるでしょう。
Nuxtエコシステムは、自動インポートやTypeScriptの改善、パフォーマンス向上など、開発体験を向上させることに重点を置いています。これらのツールは直接props
の変更を防ぐものではありませんが、Vueの設計原則に従うことでこの問題を回避できるよう促しています。
結論
Vueにおいて、props
を直接または間接的に変更することは、予測不能な動作やデバッグ困難なバグにつながるアンチパターンです。props
の特性やVueのリアクティブシステムを正しく理解したうえで、最適な対処法を選択することが重要です。この記事で紹介したベストプラクティスを参考に、よりクリーンで保守性の高いコードを目指しましょう。
-
props
の浅いコピーまたは深いコピーを作成する -
structuredClone
やcomputed
を活用する -
readonly
で不変性を保証する
これらのベストプラクティスを採用することで、よりクリーンで予測可能なコードベースを維持し、コンポーネントの保守性を向上させることができます。