2020年後半版Vue.jsを使ってる人には必ず知っていてほしいドキュメントに書かれていないComposition APIのコードの書き方とVuex vs provide/inject

# まえがき

最近[iCARE](https://www.icare.jpn.com/dev)さんの所で `Vue.js` を一緒にやらせていただいているのですが、フロントの技術スタックがかなりモダンであり、開発体験が良く、ノウハウをどんどん公開して良いと言っていただけたので、その内容を共有するシリーズです.
今回の記事の内容は[iCareさんのDev Meetup](https://icare.connpass.com/event/183716/)で話した内容になります.

最近公式のソースを追った所、 `Composition API` はapiの紹介はあれども、コードの書き方やその背景、Tips等は全然見当たりませんでした.
また、すごく強力なapiである `provide/inject` の紹介も全然見当たりません.
今回は[iCARE](https://www.icare.jpn.com/dev)さんの所で `Vue.js` を書く際に皆で意識している `Composition API` と活用している `provide/inject` のノウハウとその詳細な理由を共有します.

※ 直接編集リクエストをくれていつもありがとうございます. いつも通り記事の内容に意見がありましたら直接編集リクエストをください.

## Composition API のコードの書き方

### はじめに

`Composition API` のソース・コードは、ルールを守って書いた上だと可読性・再利用性・凝集性を向上させれるポテンシャルがありますが、ベストプラクティスを十分学ぶ前に自由に書いた場合のコードではそれらのメリットが失われおり、むしろ悪化する事があります.
ここでは銀の弾丸ではない `Composition API` を利用する上で必要な知識を学んで行きます.

### 用語

`React hooks` を利用して作られた `Component` の中で利用する為の関数の事を `Custom Hooks` と呼びます.
`Composition API` を利用して作られた関数は、rfc では `composition function` と呼ばれていましたので、その様に呼んでいきます. (一応Vue.js公式イベントでも `composition function` と読んでいたのでこれで良さそう

ソース: https://vue-composition-api-rfc.netlify.app/api.html

### Composition APIのコンセプト

`Composition API` のコンセプトは、提供されているAPIを利用して `composition function` を作成していき、作成した `composition function` 達を組み合わせて、その `Component` の `render関数` に必要な最低限の `event handler` や、 `reactive` な文言等のコンテキストを決定します.
これ自体は既出の一般的なアイディアであり、テクニックです.

### setup

`Vue 3.x` では `ComponentOptions` に `setupメソッド` を追加できます.
`Composition API` を利用するには `setupメソッド` を実装する必要があります.
`Vue 2.x` で `Composition API` を利用するためには `npm` に登録されている `@vue/composition-api` を `install` し、`Composition API` を利用するための準備をします.

```javascript
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
```

その後 `Vue 3.x` のように `Composition API` を使いたい場所で `defineComponent` 関数を利用すれば、`ComponentOptions` に `setupメソッド` を実装できます.
以下が実装例です.

```javascript
import { defineComponent } from '@vue/composition-api'

export default defineComponent({
  name: 'HelloWorld',
  props: {
    counter: {
      type: Number,
      required: true,
    },
  },
  setup(props, context) {
  }
})
```

### setupメソッドの解説

`setupメソッド` の第1引数には解決された `props` が渡されます.
`setupメソッド` の `this` には `undefined` が束縛されています.
`this` の代わりに第2引数の `context` が提供されています.
`context` の中には以前 `this` でアクセスしていた多くの項目が存在します.
以下は `@vue/composition-api@1.0.0-beta.10` で提供されている `setupメソッド` の型定義です.

```typescript
declare type SetupFunction<Props, RawBindings = {}> = (this: void, props: Props, ctx: SetupContext) => RawBindings | (() => VNode | null) | void;

interface SetupContext {
  readonly attrs: Record<string, string>;
  readonly slots: {
    [key: string]: (...args: any[]) => VNode[];
  };
  readonly parent: ComponentInstance | null;
  readonly root: ComponentInstance;
  readonly listeners: {
    [key: string]: Function;
  };
  emit(event: string, ...args: any[]): void;
}
```

### write composition function code for setup

今回サンプルコードではカウンターアプリを作ってみます.

まずは `Composition API` を使わないで書いた例です.

```javascript
import Vue from 'vue'

export default Vue.extend({
  template: `
<div>
  <div>count: {{ count }}</div>
  <button @click="inc">inc</button>
  <button @click="dec">dec</button>
  <button @click="reset">reset</button>
</div>
  `,
  name: 'SampleCounter',
  props: {
    countNum: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      count: this.countNum
    }
  },
  methods: {
    set(count) {
      this.count = count
    },
    inc() {
      this.count++;
    },
    dec() {
      this.count--;
    }
  },
  watch: {
    count(count) {
      this.$emit('change', count)
    },
    countNum(count) {
      this.set(count)
    }
  },
})
```

今までの書き方だとn箇所でカウントロジックを利用したい場合、 `mixin` を作って再利用をするのは1つの手段です.
しかし `mixin` が抱えている問題を持ち込みたくない自分みたいな人だと、n箇所に似たようなコードを書く方を選択します.

`Composition API` を利用すれば、`mixin` の課題を解決した `composition function` を作成できます.
以下が例です.

```javascript
import { reactive, computed } from '@vue/composition-api'

function useCounter(initial = 0) {
  const state = reactive({ count: initial });
  return {
    count: computed(() => state.count),
    set: count => {
      state.count = count
    },
    inc: () => {
      state.count++;
    },
    dec: () => {
      state.count--;
    }
  };
}
```

`Composition API` のおかげでカウンターに関連するコードを纏めることができました.
以下が `useCounter` の使用例です.

```javascript
defineComponent({
  template: `
<div>
  <div>count: {{ count }}</div>
  <button @click="inc">inc</button>
  <button @click="dec">dec</button>
</div>
  `,
  name: 'SampleCounter',
  props: {
    countNum: {
      type: Number,
      default: 0
    }
  },
  setup(props, context) {
    const counterContext = useCounter(props.countNum)
    watch(() => props.countNum, count => counterContext.set(count))
    watch(counterContext.count, count => context.emit(count))
    return counterContext
  },
})
```

n箇所でカウントロジックを利用したい場合、今回作成した `composition function` をn箇所で使う事ができます.
`mixin` が抱えていた衝突等の問題は一切ありません.

### Composition APIの幾つかの注意事項

こちらのドキュメントを見てみます.
https://composition-api.vuejs.org/#basic-example

`Lifecycle Hooks`の項目に書かれている一文を紹介します.

> These lifecycle registration methods can only be used during the invocation of a setup hook. It automatically figures out the current instance calling the setup hook using internal global state.

これの意味は以下の通りです

> これらのライフサイクル登録メソッドは、`setupフック` の呼び出し時にのみ使用できます。内部のグローバル状態を利用して、 `setupフック` を呼び出している現在のインスタンスを自動的に把握します。

つまり、`onMounted` 等のライフサイクルに登録する関数は `setupメソッド` の実行時にしか利用できず、`setTimeout` で非同期に、もしくは `watch` 等で非同期に登録することはできません.
これと同じ制約をもっているapiをもう1つ紹介します.
それは、 `provide/inject` です.
これも内部のグローバル状態に保持されている現在のインスタンスを利用するapiです.

### composition functionの注意事項

幾つかの `Composition API` は `setupフック` の呼び出し時にのみ使用できるapiがあります.
それらを 利用している/将来的に利用する 予定のある `composition function` も同様に `setupフック` の呼び出し時以外では使用できなくなります. (もし `setupフック` の呼び出し時以外で `composition function` を呼出した場合、その `composition function` は将来に新たに追加された `setupフック` 呼出し時でしか使えない便利なapi又は同様の制約を抱えている関連ライブラリ等も全て利用できなくなるということです.

関数を利用する際に、作られた関数のインターフェースは理解する必要がありますが、程度によりますが、基本的には詳細な実装を全て追ってない状態で、この様な関数は利用されても大丈夫です.
以上のことから、 `composition function` は、 `setupフック` の呼び出し時にのみ使用する事で、不用意なバグや事故を回避し、恩恵を受け続けることができます.
恩恵を受け続ける為に、 `setupフック` の中でのみ、 `composition function` を使用するようにすべきです.
つまり、 `setTimeout` や `computed`, `watch` 、 `event handler` の中等で `composition function` を呼出してはいけません.

### setupフック呼出し後に生成させるrefやreactive等

`setupフック` 呼出し完了後に `ref` や `reactive` 等を生成する事は可能です.
例えば、`computed` の中で `ref` を作成したり、`reactive` を作成することが可能です.
コードレビューしているとその様なコードでは `ref` や `reactive` が必要ない場合が全てでした.
`setupフック` の呼び出し完了後に `ref` や `reactive` を作らなければ実装できない物は今の所は存在しないし、今後も存在しないはずです.

### 制限のまとめ

制限をまとめると、`setupフック` の呼出し時に `computed` や `メソッド` 等の参照する値は全てそれまでに束縛されており、 `setupフック` 呼出し完了後に束縛対象が消えたり、新たに産まれたり、 `composition function` が呼び出されたりする事はないということです.

以下は準拠している例と違反している例です.

まずは、準拠している例です.
これは共有される `counter` を利用している例です.
`setupフック` の呼出し時に全ての依存の束縛が完了しており、その後に `composition function` を呼出したりしていません.
これは正しく動作します.
CodeSandboxはこちらです.
https://codesandbox.io/s/vue-composition-api-forked-1q916

[![Image from Gyazo](https://i.gyazo.com/afffb565d6dad44fce0b185a650dec80.gif)](https://gyazo.com/afffb565d6dad44fce0b185a650dec80)

```javascript
import Vue from "vue";
import VueCompositionApi from "@vue/composition-api";
import { inject, provide } from "@vue/composition-api";
import { defineComponent, reactive, computed } from "@vue/composition-api";

Vue.use(VueCompositionApi);

function createContext(contextName = "") {
  const contextKey = Symbol(contextName); // Reflect.ownKeys をサポートしていない環境もサポートする場合はuuid生成のライブラリをベースにkeyの構築をする方が良いかも
  return [
    contextValue => provide(contextKey, contextValue),
    () => inject(contextKey)
  ];
}

const [useSharedCounterProvider, useSharedCounter] = createContext(
  "SharedCounter"
);

function useCounter(initial = 0) {
  const state = reactive({ count: initial });
  return {
    count: computed(() => state.count),
    set: count => {
      state.count = count;
    },
    inc: () => {
      state.count++;
    },
    dec: () => {
      state.count--;
    }
  };
}

const providerComponentFactory = (providerHooks, providerName = "") =>
  defineComponent({
    template: "<div><slot/></div>", // Vue 3.xではfragmentが使える
    name: providerName,
    setup() {
      useSharedCounterProvider(providerHooks());
    }
  });

// useCounterの実行結果をprovideするコンポーネントの作成
const SharedCounterProvider = providerComponentFactory(
  useCounter,
  "SharedCounterProvider"
);

const SampleInjectCounter = defineComponent({
  template: `
<div>
  <div>count: {{ count }}</div>
  <button @click="inc">inc</button>
  <button @click="dec">dec</button>
</div>
  `,
  name: "SampleInjectCounter",
  setup() {
    return useSharedCounter();
  }
});

new Vue({
  components: {
    SharedCounterProvider,
    SampleInjectCounter
  },
  template: `
<shared-counter-provider>
  <sample-inject-counter/>
  <sample-inject-counter/>
  <shared-counter-provider>
    <sample-inject-counter/>
  </shared-counter-provider>
</shared-counter-provider>
  `,
  name: "SampleApp"
}).$mount("#app");
```

次は `event handler` で `composition function` を呼出している例です. (違反例
これは `event handler` が `Error` を吐きます.
CodeSandboxはこちら
https://codesandbox.io/s/vue-composition-api-forked-onjbj

`invalidInc` をクリックすると以下のように `Error` がでます.
[![Image from Gyazo](https://i.gyazo.com/28a574772778f730e8aee2ac727ff82b.gif)](https://gyazo.com/28a574772778f730e8aee2ac727ff82b)

```javascript
const InvalidSampleInjectCounter = defineComponent({
  template: `
<div>
  <div>invalidComputed: {{ invalidComputed }}</div>
  <button @click="invalidInc">invalidInc</button>
  <button @click="invalidDec">invalidDec</button>
</div>
  `,
  name: "InvalidSampleInjectCounter",
  setup() {
    const { count } = useSharedCounter();
    return {
      invalidComputed: computed(() => count.value ** 2),
      invalidInc: () => useSharedCounter().inc(),
      invalidDec: () => useSharedCounter().dec()
    };
  }
});
```

`Errorメッセージ` はこの通りで、この `Error` は想定通りです. `[Vue warn]: inject() can only be used inside setup() or functional components.`

### Composition APIのまとめ

基本的に `Composition API` を利用すると今までの `Option API` にあった制限やスコープの影響で正常動作しないリスクのあったいくつかの手法が解禁されます.
それらの手法を使わない場合は `Option API` の方が上手く書ける場合は多々あります.
今までの `Option API` で良くも悪くも慣れている状況下で `Composition API` をチームでいきなり使いこなすには超えるハードルが多く、その為チーム一丸となって段階的に浸透させる戦略等をチームに合う形で考える事が必要かもしれないです.

## Vuex vs provide/inject

共有ステートを作成する時に、`Vue.js公式` からは幾つかの手段が用意されています.
以下がその一覧です. (他にもあるかも

1. [Vue.observable](https://jp.vuejs.org/v2/api/index.html#Vue-observable)
2. [Vuex](https://vuex.vuejs.org/ja/)
3. dataを共有する専用の `Vue.js` インスタンスの作成
4. [provide/inject](https://jp.vuejs.org/v2/api/index.html#provide-inject)

共有ステートを作成したい要望がある時、最近だと `Vue.observable` も有力な選択肢に入ってきます.
しかし、`Vue.observable` で `state` を `global` に作成して利用する場合は、`singleton` であり、`singleton` が持つ新たな課題を産みます. (例えば、同一プロセスでのテストの同時実行数への制限や、各テストで `singleton` の初期化を忘れない、差し替え難易度等... etc

例:

```javascript
// これはsingletonなglobal stateで新たな課題を産みます.
export const sharedCounter = Vue.observable({
  count: 0
})
```

以前だと多くのプロジェクトで `Vuex` を使うか使わないかの話になる場合が多いです.
`Vuex` は分離できない単一の大きなモジュールのコードを複数人で書き、作成していく為、複数人で単一のモジュールを開発する文脈由来の課題が発生し、品質維持の難易度が高いです. `Vuex` を初めて導入した後の多くのプロジェクトでは可能なら多くのコードを書き直したいと考えているチームは多いはずです.

provide/inject は小さく分離できる小さなモジュールをアプリケーション 全体/部分 で簡単に共有できるものであり、provideするものをある程度まで小さくすれば影響範囲の把握がしやすいです.
`provide/inject` の乱用もよくありませんが、認証情報、環境依存情報、一部のコンポーネント専用の情報(Form等)等の共有を実現するのに役立ちます.
当然 `singleton` でもないですし、テストも容易です.
`provide/inject` は基本的にはVuexより手軽にできると思います.

これは先程と同じコードですが、 `provide/inject` のデモとして十分であるため、再利用させていただきます.

CodeSandboxはこちらです.
https://codesandbox.io/s/vue-composition-api-forked-1q916

[![Image from Gyazo](https://i.gyazo.com/afffb565d6dad44fce0b185a650dec80.gif)](https://gyazo.com/afffb565d6dad44fce0b185a650dec80)

stateの共有関係はこの色同士です.
![Frontend Architecture@2x (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/af3ec8a2-9d10-7677-7c73-781e1e51bb55.png)


```javascript
import Vue from "vue";
import VueCompositionApi from "@vue/composition-api";
import { inject, provide } from "@vue/composition-api";
import { defineComponent, reactive, computed } from "@vue/composition-api";

Vue.use(VueCompositionApi);

function createContext(contextName = "") {
  const contextKey = Symbol(contextName); // Reflect.ownKeys をサポートしていない環境もサポートする場合はuuid生成のライブラリをベースにkeyの構築をする方が良いかも
  return [
    contextValue => provide(contextKey, contextValue),
    () => inject(contextKey)
  ];
}

const [useSharedCounterProvider, useSharedCounter] = createContext(
  "SharedCounter"
);

function useCounter(initial = 0) {
  const state = reactive({ count: initial });
  return {
    count: computed(() => state.count),
    set: count => {
      state.count = count;
    },
    inc: () => {
      state.count++;
    },
    dec: () => {
      state.count--;
    }
  };
}

const providerComponentFactory = (providerHooks, providerName = "") =>
  defineComponent({
    template: "<div><slot/></div>", // Vue 3.xではfragmentが使える
    name: providerName,
    setup() {
      useSharedCounterProvider(providerHooks());
    }
  });

// useCounterの実行結果をprovideするコンポーネントの作成
const SharedCounterProvider = providerComponentFactory(
  useCounter,
  "SharedCounterProvider"
);

const SampleInjectCounter = defineComponent({
  template: `
<div>
  <div>count: {{ count }}</div>
  <button @click="inc">inc</button>
  <button @click="dec">dec</button>
</div>
  `,
  name: "SampleInjectCounter",
  setup() {
    return useSharedCounter();
  }
});

new Vue({
  components: {
    SharedCounterProvider,
    SampleInjectCounter
  },
  template: `
<shared-counter-provider>
  <sample-inject-counter/>
  <sample-inject-counter/>
  <shared-counter-provider>
    <sample-inject-counter/>
  </shared-counter-provider>
</shared-counter-provider>
  `,
  name: "SampleApp"
}).$mount("#app");
```

この `provide/inject` は公式ドキュメントにも書かれていますが、`React` の `Context API` と酷似しています.
`Vuex vs provide/inject`の話をもし深追いしたい場合は、`Redux vs React Context` と `Google検索` すれば、期待以上の沢山の有意義な議論を目にすることが可能です.

[iCARE](https://www.icare.jpn.com/dev) では `Vuex` を使わずに、 `provide/inject` を積極的に活用しています.

## 最後に

如何だったでしょうか？
`Vue 3.x` がリリースされ、`jsx` と `functional component` が使いやすくなる日が待ち遠しいですね.
`Fragment` や `Teleport`、`Suspense` 等も凄く楽しみです