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で不変性を保証する
これらのベストプラクティスを採用することで、よりクリーンで予測可能なコードベースを維持し、コンポーネントの保守性を向上させることができます。