【Vue 3.0】readonlyをいい感じに使う
Vue 3.0ではリアクティブな値をReadonlyプロキシに変換(≒読み取り専用に)するreadonlyという関数が提供されています。
リアクティブなデータはビジネスロジックによって変更されることが前提となっていますが1、誰がどのような操作をしても良い状態はたいへん治安が悪いです。
『readonlyな値 + その値を操作する関数』というReact Hooksで割と見慣れたパターンを採用することでコンポーネントから独立した状態管理を作ることができ、疎結合かつ副作用が限定的なコードを書くことができます。
ほとんどのケースにおいては読み取り専用の値で用が足りるはずなので、単純なリアクティブデータでも関数を通じてコンポーネントに合成するようにしておくと、 将来的にその値に何らかの操作がスタイルが有用かもしれません。
readonlyってなに?
readonlyとはreactiveやrefの値を引数にとり、その値を読み取り専用としてマークするための関数です。 setterを指定しない場合のcomputedの返り値の型もこのReadonlyになっています。
リアクティブでないオブジェクトやプリミティブ型の値を読み取り専用にすることもできますが、 このような値への再代入はそもそも避けるべきなので通常はわざわざreactiveにする意味がないと思います。
TypeScriptを使っている場合はReadonly型の値になり、型レベルで読み取り専用が保証できます。 template内ではトランスパイル時にエラーとはなりませんが、イベントハンドラなどを用いて値を書き換えようとすると失敗します。
provide/injectではreadonly化を忘れずに
特にprovide/injectを使って状態を複数コンポーネントで共有する場合はReadonlyプロキシの利用がほぼ必須となります。 何も考えずにrefやreactiveを返却すると、どのコンポーネントからも値を変更できてしまうため好き勝手なロジックで状態を書き換えられてしまったり、 コンポーネント間の結合が複雑になったりといいことがありません。
以下のサンプルのようにコンポーネントに提供する値はreadonlyプロキシに変換し、その値を書き換えるための関数を返り値に含めると良いでしょう。
import { InjectionKey, reactive, provide, inject, readonly } from 'vue'
type User = {
name: string
//......
}
export const CurrentUser: InjectionKey<{ currentUser: Ref<User> }> = Symbol()
export const provideCurrentUser = () => {
const currentUser = reactive<User | null>(null)
// (currentUserを取得・更新する処理...)
provide(CurrentUser, { currentUser })
}
export const useCurrentUser = () => {
const value = inject(CurrentUser)
if (injectedValue === undefined) {
throw new Error('currentUser is not provided')
}
// currentUserに対する操作
const setName = (name: string) => {
value.currentUser.name = name
// ......
}
return {
currentUser: readonly(injectedValue.currentUser),
setName
}
}
これはシンプルな例ですが、値に対する操作がビジネスロジックを含むケースでこのパターンが有用であることが実感できると思います。 たとえば、値を書き換える前にバリデーションが必要だったりとか、同時にAPI呼び出したりだとか、そういうことをやろうとしたときに関数がセットで提供されていて、 余計な副作用の心配をしなくていいようになっているという安心感が得られるのはありがたいです。
- 変更しないのであればリアクティブにする必要はありません。単純な変数をconstで定義すればOKです。↩