Composition APIによる Provide / Inject を整理

## 概要

参画先にて、状態管理の方法の一つとして、provide / injectを使用しており、
composableなどどごっちゃになりかけたので整理するための投稿です。

## 環境
- Nuxt.js
    - composition API
    - vue 2.7.3


## Provide / Inject とは

親コンポーネントから子供コンポーネントへ状態を共有できる仕組みのことです。

親コンポーネントから提供（Provide）し、
子コンポーネントに注入する（Inject）
といったところでしょうか。

データを子コンポーネントに渡す方法として、他にはpropsがあります。
一つ下の階層のコンポーネントに渡すには良いですが、
親からより深い階層へデータを渡すとなると、propsを渡し続けることになりそうです。。
考えただけでも恐ろしいですね。

そこで、親コンポーネントでprovideを定義し、
使いたい子コンポーネントのところでinjectすると、
遠い階層のコンポーネントでもpropsでバケツリレーせずにデータの共有ができます。

動作が見えるようにコードを書いてみました。
ボタンを押すと、正方形が現れるコンポーネントです。
状態は正方形を表示するかどうかの判定値のみを定義しております。
![provide1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697823/d04a4521-30c8-75a0-bbd1-9553306954a1.gif)


これを同じページ内に3個表示されるようにしました。

状態をコンポーネント内に定義した場合、
これらは個別に表示することができます。
![provide2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697823/e375c756-bb72-1cb1-a88c-d6b2a2b70c79.gif)


これらをprovide / injectで管理にしてみると、
ボタンをクリックすると3個全ての正方形が表示されるようになります。

つまり、全て同じデータを参照していることがわかります。

![provide3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697823/dc4c6cef-de3c-6309-b58a-8989b099ac36.gif)

## 使い方

Provide / Injectを使用するにあたり、以下の設定をする必要があります

1. Provideに関する定義をする
2. Injectに関する定義をする

### 1. Provideに関する定義をする

ここではデータを外部にProvideするために、以下の定義をします。

- 公開するデータの定義
- データを操作する関数の定義
- キーの定義
- provide関数を作成し、default.vueで呼び出す


ダイアログコンポーネントを例にコードを書いていきます。

**公開するデータの定義**
まずは、公開するデータを定義します。
データが変更したらコンポーネントに反映させたいので、reactiveなデータにします。

```tsx
type DialogStateType = {
	isShow: boolean
}

const dialogStore = () => {
  const state = reactive<DialogStateType>({
    isShow: false
  })
}
```

**データを操作する関数の定義**
データを操作する関数を定義します。

これはProvide先でデータが変更されることを防ぐため、
Provideするデータはreadonlyにすることが公式で推奨されているようです。

Fluxのようにデータの流れを制御することで、
予期せぬバグを防ぐ意図があるのかなと個人的には思っています。

```tsx
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<T>型で定義できます。

ですが以下のメリットもあるため、 `InjectionKey型` を使用するのが良さそうです。

- Symbol型で代入するため、キーの重複を防ぐことができる
- provide関数にて、provideするデータを型チェックすることができる
- inject関数にて、戻り値が型推論される

極端ですが、NGの例とOKの例を書いてみました。

```tsx
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関数はラッピングしてあげます。
```tsx
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することがシンプルな解決方法のようです。

```tsx
const useDialogStore = () => {
  const store = inject(dialogStoreKey);
  if (!store) throw new Error('DialogStore is undefined')
  return store
}
```

出来上がったコードがこちら

```tsx
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のメリデメあたりをまとめられたらなと思っております。

## 参考記事
- [Provide / Inject](https://vuejs.org/guide/components/provide-inject.html)
- [Provide / Inject(composition-api)](https://v3.ja.vuejs.org/guide/composition-api-provide-inject.html)
- [type-safe-provide-inject](https://logaretm.com/blog/type-safe-provide-inject/)