vue composition apiにおけるpropsのリアクティブ性について知っておくべきこと
この記事で伝えたいこと
- vue composition apiにおけるコンポーネントの
setup
で受け取れるprops
はreactive
で定義した変数のようなリアクティブ性をもちます -
props
のプロパティにリアクティブ性を求めるパターンと求めないパターンごとの使い分け
ref
, reactive
, computed
リアクティブをもたらすAPI composition apiではリアクティブをもたらすAPIとしてref
, reactive
, computed
などがあり、特にref
とreactive
に関してはどちらをつかうべきかという記事が多い印象をもちます。
それぞれの比較としては他開発者さまの記事ですがこちらが大変参考になると思います。
私自身振り返って考えてみるとref
とcomputed
を多用しています。
ref
はTypeScriptを使っていればリアクティブな変数かどうかわかりやすいし、
computed
は他のリアクティブな変数に依存するリアクティブな変数を簡潔に表現でき、かつref
のように別のロジックから変更される恐れがないので使いやすいです。
const isOpen = ref(true);
const message = computed(() => isOpen.value ? '開いてるよ!' : '閉まっているよ!');
// messageに入る値は上記の2パターンのみであり更新はされない。下記を実行すると `Write operation failed: computed value is readonly` というワーニングが表示される
message.value = '全然関係ないメッセージ';
UIロジックなどを表現する際にはref
とcomputed
でほぼ十分でありますが、storeを利用する場合などではreactive
を用います。
しかしreactive
にはリアクティブの消失という注意ポイントがあります。
reactive
のリアクティブの消失について簡単に言うと
リアクティブの消失についてはリアクティブな状態の分割代入にある通りで分割代入によってリアクティブを消失します。
分割代入の例としては下記のようなパターンもあります。
const state = reactive({ name: 'karino' });
// 1. リンクにある通りの分割代入を行うパターン
const { name } = state;
// 2. プロパティを直接参照して代入するパターン
const name = state.name;
// 3. プロパティを直接参照して他の処理に渡すパターン
const { length } = useLength(state.name);
// useLengthの戻り値のlengthはリアクティブな変数のはずですが、state.nameが更新されてもその値が変わることはないです
// 一種のcomposable
const useLength = (name: string) => {
const length = computed(() => name.length);
return { length };
};
これらは分割代入によってリアクティブが消失しているといえますが、別の言い方をすると 「reactive
で定義された変数自体はリアクティブだが、そのプロパティはリアクティブではない」 という表現もできるかなと思います。
実験1: どんなタイプのプロパティでもリアクティブは消失する
実験2: プロパティがrefによるものでも分割代入でリアクティブは消失する
props
は何にあたるのか
本題1: コンポーネントのsetup
の第一引数に渡されるprops
変数もリアクティブな変数です。
そしてreactive
で作られた変数のプロパティがリアクティブを消失するパターンを見ましたが、
同様にprops
も同じ操作でそのプロパティのリアクティブを消失します。
例: propsは分割代入でリアクティブを失う
上記実験1でのリンク先ではreactive
で作成した変数の分割代入された変数がリアクティブを失っているのと同じように、props
のプロパティも分割代入によってリアクティブを失っています。
このことからprops
も reactive
で定義された変数と同じような リアクティブ性を持つと考えられます。
props
のプロパティにリアクティブを求めるパターンと求めないパターンの使い分け
本題2: props
のプロパティがリアクティブを失うパターンについて見ました。
ではprops
のリアクティブを生かした状態でロジックを書きたい時と、そうでない時ではそれぞれどのように使い分ける必要があるのかを書き方ごとにまとめてみます。
props
のプロパティにリアクティブを求めるパターン
props
をそのまま使える場合はリアクティブのまま
上記まででprops
のプロパティがリアクティブを失うパターンを見ましたが、いずれも分割代入の実行タイミングがsetup
の直下でした。
分割代入の実行タイミングが変われば、その時のプロパティの値が取得できます。
下記例ではボタンを押すとその時のid
の値を用いてsomeAPI
を呼びます。
<template>
<button @click="callAPI">call api</button>
</template>
<script>
export default {
props: { id: { type: Number } },
setup(props) {
const callAPI = () => {
someAPI(props.id);
};
return { callAPI };
},
};
</script>
props
+ toRefs
, toRef
toRefs
, toRef
はいずれもreactive
な変数に使えますしprops
にも使えます。
例: props
にtoRef
を使い、親・子からそれぞれ更新してみる実験
親から子の変更は伝わりますが、子で取得したリアクティブな変数を更新しようとするとSet operation on key "〇〇" failed: target is readonly.
というワーニングが出て更新されません。
これはprops
の子側からの変更を許さないVueの方針によるものです。
この方法のメリットはprops
のプロパティを独立したリアクティブな変数として宣言できるところにあります。
これを行うことでプロパティのみをcomposable
なメソッドに与えロジックの分割ができるようになります。
しかしTypeScriptを用いている場合、toRefs
, toRef
によって取得した変数はRef
型になり、〇〇.value = △△
で型エラーになりません。
もしそのようなコードがある場合、実行時不具合になりエラーログではなくアラートログが出るだけなので、発見や修正が遅れるかも…しれません。ちょっと怖いですね。
今後のVue自体のアップデートで対応されたり、もしかしたらLintの設定などで対処できるのか分かりませんが、一先ず注意ポイントですね。
props
+ computed
computed
を使うことでもprops
のプロパティをリアクティブな変数として独立させることができます。
個人的にはtoRef
, toRefs
を使うよりもいいのではないかと思っています。
export default {
props: { name: { type: String } },
setup(props) {
// プロパティに連動した別のリアクティブな変数を作るのに便利
const nameLength = computed(() => props.name.length);
// プロパティとまったく同じ意味合いのリアクティブな変数を作ることも可能
const name = computed(() => props.name);
},
};
computed
をおすすめする理由は2つあります。
1つ目はTypeScript上ではComputedRef
型になる点です。
ComputedRef
型はReadOnlyな型なので〇〇.value = △△
で型エラーになります。実行前に不備を気づけるようになります。
2つ目は覚えるAPIの数を減らせるという点です。
computed
は便利な特性をもつので積極的に使い覚えたいAPIですが、toRef
,toRefs
は比較的使用頻度は高くないです。
toRefs
は一気に複数のプロパティを取り出せる使い方もできますが、少なくともtoRef
に限って言えばcomputed
でいいのではないでしょうか?
props
のプロパティにリアクティブを求めないパターン
const 〇〇 = props.〇〇
setup
の直下でこの書き方をするということは、コンポーネントがマウントされて以降更新されない値だということになります。
props
+ ref
const input = ref(props.initialValue);
const onUpdate = (inputValue: string) => input.value = inputValue;
上記例のようにref
の中でprops
のプロパティを使う場合は、そのref
の変数の初期値としてprops
のプロパティを使うという意味になります。
props
のプロパティと連動してref
の変数が変わることはありません。
props
のプロパティに対応する変数が親のコンポーネント側で更新されても、子のコンポーネントのref
は更新されませんし、逆に子のref
が更新されても親の値は変わりません。
最後に
なぜこのような当たり前のような記事を今更書いたかというと、実は私はprops
がリアクティブじゃなくなる条件をよく分からないまま使っていたからです。
しかし考えてみればreactive
と同じような動きをすることに気づき、自分の鈍感さを痛感しました。
あまり良くないロジックを量産していたような気がします。恥ずかしい〜
以上
Discussion