概要
参画先にて、状態管理の方法の一つとして、provide / injectを使用しており、
composableなどどごっちゃになりかけたので整理するための投稿です。
環境
- Nuxt.js
- composition API
- vue 2.7.3
Provide / Inject とは
親コンポーネントから子供コンポーネントへ状態を共有できる仕組みのことです。
親コンポーネントから提供(Provide)し、
子コンポーネントに注入する(Inject)
といったところでしょうか。
データを子コンポーネントに渡す方法として、他にはpropsがあります。
一つ下の階層のコンポーネントに渡すには良いですが、
親からより深い階層へデータを渡すとなると、propsを渡し続けることになりそうです。。
考えただけでも恐ろしいですね。
そこで、親コンポーネントでprovideを定義し、
使いたい子コンポーネントのところでinjectすると、
遠い階層のコンポーネントでもpropsでバケツリレーせずにデータの共有ができます。
動作が見えるようにコードを書いてみました。
ボタンを押すと、正方形が現れるコンポーネントです。
状態は正方形を表示するかどうかの判定値のみを定義しております。
これを同じページ内に3個表示されるようにしました。
状態をコンポーネント内に定義した場合、
これらは個別に表示することができます。
これらをprovide / injectで管理にしてみると、
ボタンをクリックすると3個全ての正方形が表示されるようになります。
つまり、全て同じデータを参照していることがわかります。
使い方
Provide / Injectを使用するにあたり、以下の設定をする必要があります
- Provideに関する定義をする
- Injectに関する定義をする
1. Provideに関する定義をする
ここではデータを外部にProvideするために、以下の定義をします。
- 公開するデータの定義
- データを操作する関数の定義
- キーの定義
- provide関数を作成し、default.vueで呼び出す
ダイアログコンポーネントを例にコードを書いていきます。
公開するデータの定義
まずは、公開するデータを定義します。
データが変更したらコンポーネントに反映させたいので、reactiveなデータにします。
type DialogStateType = {
isShow: boolean
}
const dialogStore = () => {
const state = reactive<DialogStateType>({
isShow: false
})
}
データを操作する関数の定義
データを操作する関数を定義します。
これはProvide先でデータが変更されることを防ぐため、
Provideするデータはreadonlyにすることが公式で推奨されているようです。
Fluxのようにデータの流れを制御することで、
予期せぬバグを防ぐ意図があるのかなと個人的には思っています。
const dialogStore = () => {
const state = reactive<DialogStateType>({
isShow: false
})
// new
const openDialog = () => {
state.isShow = true
}
// new
const closeDialog = () => {
state.isShow = false
}
// new
return readonly({
state,
actions: {
openDialog,
closeDialog
}
})
}
キーの定義
キーを定義します。
公式によると、キーはString型、もしくはInjectionKey型で定義できます。
ですが以下のメリットもあるため、 InjectionKey型
を使用するのが良さそうです。
- Symbol型で代入するため、キーの重複を防ぐことができる
- provide関数にて、provideするデータを型チェックすることができる
- inject関数にて、戻り値が型推論される
極端ですが、NGの例とOKの例を書いてみました。
type ExampleStoreType = {
title: string
}
const exampleStoreKey: InjectionKey<ExampleStoreType> = Symbol('exampleStore')
// NG provideするデータがExampleStoreTypeと異なるため、型エラーが発生する
provide(ExampleStoreType, '')
// OK
provide(ExampleStoreType, { title: 'よかろう。' })
provide関数を作成し、default.vueで呼び出す
作成したデータをもとにprovide関数を作成します。
作成したprovide関数はdefault.vueで呼び出します。
使いまわしやすいように、provide関数はラッピングしてあげます。
const dialogStore = () => {
const state = reactive<DialogStateType>({
isShow: false
})
const openDialog = () => {
state.isShow = true
}
const closeDialog = () => {
state.isShow = false
}
return readonly({
state,
actions: {
openDialog,
closeDialog
}
})
}
// new
export const provideDialogStore = () => {
return provide(dialogStoreKey, dialogStore())
}
provideDialogStore()
をdefault.vueで呼び出してあげましょう。
2, Injectに関する定義
Inject関数を使って、provideされた値を使用する処理を書いていきます。
Inject関数の戻り値をみてみると、Provideされる値の型 もしくは undefinedといったユニオン型になっています。
そのため、undefinedの場合を考慮する必要があります。
他の記事を見てみると、Errorをthrowすることがシンプルな解決方法のようです。
const useDialogStore = () => {
const store = inject(dialogStoreKey);
if (!store) throw new Error('DialogStore is undefined')
return store
}
出来上がったコードがこちら
import { inject, InjectionKey, provide, reactive, readonly } from "vue";
type DialogStateType = {
// 表示するかどうかの判定値
isShow: boolean
}
type DialogStoreType = ReturnType<typeof dialogStore>
const dialogStoreKey: InjectionKey<DialogStoreType> = Symbol('dialogStore')
const dialogStore = () => {
const state = reactive<DialogStateType>({
isShow: false
})
const openDialog = () => {
state.isShow = true
}
const closeDialog = () => {
state.isShow = false
}
return readonly({
state,
actions: {
openDialog,
closeDialog
}
})
}
export const provideDialogStore = () => {
return provide(dialogStoreKey, dialogStore())
}
export const useDialogStore = () => {
const store = inject(dialogStoreKey);
if (!store) throw new Error('DialogStore is undefined')
return store
}
最後に
composition-apiのおかげもあって、良くも悪くも自由データを使いまわせるようになる(というとちょっと言い過ぎかもですが)
印象があります。
グローバルに使いまわすコンポーネントに、
ドメイン固有のものをInjectしてしまうと依存が生まれてしまったりということもありそうです。
次はProvide / Injectのメリデメあたりをまとめられたらなと思っております。