Vue 3.0時代の状態管理
この記事ではVue 3.0の世界における複数のコンポーネントにまたがる状態管理について、provide/injectが便利だよっていう話をします。
単一のコンポーネントに閉じた状態管理は単純にComposition APIを利用すれば良いので、ここでは取り上げません。
provide/inject
Vue 2.xのprovide/injectは親コンポーネントのデータ・メソッドを子孫コンポーネントでも使えるようにするような機能でした。 ほとんどの場合はイベントとpropsのやりとりで用が足りるため、provide/injectを積極的に使いたいシーンはあまりなかったように思います。
一方で、Vue 3.0のprovide/injectは、コンポーネントに縛られていないデータやメソッドをコンポーネントツリーの一定の範囲に共有することができます。 もはやデータやメソッドをprovideするための(機能的な)コンポーネントは必要なくなり、全体としてVueコンポーネントは見た目を作り込む方向に集中しやすくなったのではないかと思います。
以下のコードはユーザのログイン状態をprovide/injectで提供するサンプルです。 まずはprovideとinjectする関数をコンポーネント外の適当なところに作成します。
import { InjectionKey, reactive, provide, inject } from 'vue'
type User = {
//......
}
export const CurrentUser: InjectionKey<{ currentUser: User | null }> = Symbol()
export const provideCurrentUser = () => {
const currentUser = reactive<User | null>(null)
// (currentUserを取得・更新する処理...)
provide(CurrentUser, { currentUser })
}
export const useCurrentUser = () => {
const value = inject(CurrentUser)
// provideされていないところでinjectするとundefinedが返る
if (value === undefined) {
throw new Error('currentUser is not provided')
}
return value
}
InjectionKeyの実体はSymbolですが、ジェネリクスで型を指定するとprovideとinjectに型検査が効きます。 通常は必要ありませんが、思考実験の結果によるとテストコードを書くときにproviderのモックを作りやすくなる気がするのでInjectionKeyはexportすることにしています。
使うときはsetup()内でprovideを呼びます。
<script lang="ts">
import { defineComponent } from 'vue'
import { provideCurrentUser, useCurrentUser ] from '@/compositions/user'
import Child from '@/components/Child.vue'
export default defineComponent({
components: {
Child
},
setup() {
provideCurrentUser()
// provideした直後からinject可能
const { currentUser } = useCurrentUser()
return {
currentUser
}
}
})
</script>
Vue 2.xのprovide/injectと同様に親コンポーネントでprovideされているものはすべての子孫コンポーネントでinjectできます。
<script lang="ts">
import { defineComponent } from 'vue'
import { useCurrentUser ] from '@/compositions/user'
export default defineComponent({
setup() {
const { currentUser } = useCurrentUser()
return {
currentUser
}
}
})
</script>
Vuexの出番わからん
いまのところ、Vue 3.0の世界でVuexを使いたいと思うユースケースには出会っていません。 Vuexの絡むところになんらかの修正を入れたときの影響範囲の特定が非常に困難だから、という理由が大きいです。
やはり暗黙的に状態を共有してしまうというのは、疎結合なコンポーネントをメンテナンスしつづけていく上でさまざまな課題となります。 グローバルな状態を利用するコードを書くのは(そうすべきかどうかという判断をすることも含めて)非常にストレスが高い作業です。 そんなところに精神ポイントを払うくらいならprovide/injectを使ったほうがいいと思います。
Vuexを使わなくても、ルートコンポーネントでprovideを呼ぶことで実質グローバルな状態・シングルトン的なインスタンスを作るれます。 その状態を利用したい箇所では明示的にinjectをしなければならないので、依存を発見するのも難しくありません。
もしVuexを優先して使うとしたら、暗黙的な状態の共有を行うことにメリットがあるケースまたはprovide/injectが使えないコンテキストでVuexなら使えるケースになると思いますが、 それがどんな状況なのかはよくわかりません。
コンポーネントの状態管理はなるべくスコープを小さく
メンテナンスしやすいコードを書くためには、コンポーネントの持つ状態をなるべくシンプルに扱うことがポイントになります。 Vue 3.0以降では、その手法としてprovide/injectが重要な役割を果たす気がします。
Reactの世界でもuseContextやRecoilのようにスコープの小さい状態管理に注目が集まっているようです。 安易に「Vueで状態管理→Vuexだ!」とならずに、provide/injectを試してみるのも良いかと思います。