175
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Vue3 Composition APIにおいて、Providerパターン(provide/inject)の使い方と、なぜ重要なのか、理解する。

はじめに

Composition APIで、Providerパターン(provide/inject)が、なぜ重要なのでしょうか。

Composition APIを使えば「状態やロジック」を簡単にコンポーネントの外に切り出すことができます。

スクリーンショット 2020-07-17 15.47.53.png

しかしながら、Composition APIだけでは「ロジックや状態は切り出せても、複数コンポーネント間で共有できない」からです。

Composition APIによって、荷物をダンボールに梱包はできても運ぶ人がいない状態です。

スクリーンショット 2020-07-17 17.49.06.png

みなさんは、VueのProviderパターン(provide/inject)って使ったことありますか?
https://jp.vuejs.org/v2/api/index.html#provide-inject

「ん、Provider? なにそれ?」って感じですかね。

Providerパターンとは、「荷物を運んで、受け渡す」仕組みのことです。

nimotsu_ukewatashi.png

Providerを使うと、親コンポーネントから子コンポーネントに共通の状態を簡単に受け渡すことができます。
状態を渡す先は、子コンポーネントであればどこでも渡すことができます。
Prop渡しと違ってバケツリレーをする必要がありません。

スクリーンショット 2020-07-24 9.36.04.png

私は、Providerを使ったことありませんでした。
そもそも、存在すら知りませんでした。

私と同じように、Providerなんて、知らないし、使ったことない人が多いんじゃないかなと思います。

そこで、Providerの「使い方」や「なぜ使うのか」といった内容を、本記事にまとめてみようかと思います。

Remote.vueがきっかけでProviderを知る

先月行われた「Remote.vue #1」に、視聴枠で参加しました。

動画配信URL: https://youtu.be/rx249I9JmNU

IMAGE ALT TEXT HERE

来たるVue3時代を見据えた発表内容に、心躍らせました。
特に興味深かったのは、Vue3時代の設計についてフォーカスされていたセッションです。

thumbnail

登壇者の方から「Provider」「provide/inject」という単語を頻繁に聞きました。
私は、その時は、「Providerってナニソレ美味しいの?」状態でした。
今になってようやくProviderの重要性に気づいたので、当記事に内容をまとめています。

この記事が、誰かのお役に立てれば幸いです。

次回「Remote.vue #2」は、7/31に開催みたいです。楽しみですね。
https://lapras.connpass.com/event/179777/

Providerパターンとは

Vueにおける「Providerパターン」とは、「provide/inject」APIを使用して、Composition APIで切り出した「状態やロジック」「コンポーネント間で共有すること」を指します。

Vue3時代は、Providerが重要になる

provide/injectは、Vue2.2から使用できるAPIになります。
Vue3から新しく搭載された機能かと勘違いしていたのですが、意外に古くからある機能なんですね。

provide / inject

 2.2.0 から新規

型:

 provide: Object | () => Object
inject: Array | { [key: string]: string | Symbol | Object }

詳細:

この 1 組のオプションは、コンポーネントの階層がどれほど深いかにかかわらず、それらが同じ親チェーン内にある限り、祖先コンポーネントが、自身の子孫コンポーネント全てに対する依存オブジェクトの注入役を務めることができるようにするために利用されます。React に精通している人は、 React のコンテキストの特徴と非常によく似ていると捉えると良いでしょう。

Providerパターンは、Vue 2.x時代は影が薄かったのですが、Vue 3のリリースが迫るにつれにわかに注目されつつあります。

注目される理由は、Vue3のComposition APIと関連があります。

Composition APIを使えば、「状態とロジック」を切り出せる

Composition APIについては、稚拙ながら以下の記事を書きましたので、合わせてご覧くださると理解が深まるかと思います。

Composition APIを使えば、ロジックを再利用するために、コンポーネントから「状態とロジック」を切り出すことができます。

ここからは、簡単なカウンターアプリのコードを見ながら説明します。

スクリーンショット 2020-07-16 17.17.29.png

このカウンターアプリは、「+」を押すと+1カウントアップされ、「-」を押すと-1カウントダウンするという単純なものです。

スクリーンショット 2020-07-16 17.17.44.png
「+」を押したら、カウントアップした

コンポーネントに「View、状態、ロジック」が混在した状態

上記のカウンターアプリを、VueのComposition APIを使ってSFCファイルに書くと、以下のようになります。

CounterApp.vue
<template>
  <div>
    <!-- View -->
    <div>{{ state.count }}</div>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from "@vue/composition-api"

export default defineComponent({
  setup() {
    // 状態
    const state = reactive<{
      count: number;
    }>({
      count: 0
    })

    // ロジック
    const increment = () => state.count++
    const decrement = () => state.count--

    return {
      state,
      increment,
      decrement,
    }
  }
})
</script>

上記のコードの問題点として、コンポーネントの中に「View、状態、ロジック」が混在しています。

スクリーンショット 2020-07-17 15.29.55.png

コンポーネントが肥大化すると、可読性が下がったりメンテナンスしにくくなったり、様々な問題を引き起こします。

スクリーンショット 2020-07-17 15.53.47.png

そこで、コンポーネントの中から「状態とロジック」を外に切り出します。
Composition APIを使っているので、簡単に外に切り出すことができます。

スクリーンショット 2020-07-17 15.47.53.png

「状態とロジック」を切り出したコードは、以下のようになります。

コンポーネントから、「状態とロジック」を外に切り出す

「useCounter」として、「状態とロジック」をコンポーネントの外に定義します。

composables/use-counter.ts
import { reactive } from "@vue/composition-api"

export default function useCounter() {
  // 状態
  const state = reactive<{
    count: number;
  }>({
    count: 0
  })

  // ロジック
  const increment = () => state.count++
  const decrement = () => state.count--

  return {
    state,
    increment,
    decrement,
  }
}

コンポーネント側は、useCounterをimportして、使うだけになります。

CounterApp.vue
<template>
  <div>
    <div>{{ state.count }}</div>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
import useCounter from "@/composables/use-counter"

export default defineComponent({
  setup() {
    // 状態とロジックを外部に切り出せた!
    const { state, increment, decrement } = useCounter()
    return {
      state,
      increment,
      decrement,
    }
  }
})
</script>

ほら、スッキリしましたね。

ここまでの説明で、Composition APIを使えば、「状態とロジック」をコンポーネントの外に切り出せることがわかっていただけたかと思います。

続きまして、本題のProviderについて、説明していこうと思います。

なぜ、Vue3時代は、Providerが重要になるのか?

結論から申しますと、Composition APIだけでは「ロジックや状態は切り出せても、共有できない」からです。

character_danbo-ru_walk.png

Composition APIによって、荷物をダンボールに梱包はできたのですが、運ぶ人がいない状態です。
ダンボールの中身を他の部屋の人が使いたいと思っても、使えません。
ダンボールに勝手に手足が生えて、勝手に配達先まで歩いていってはくれないのです。

Composition APIを使えば、「ロジックや状態」を切り出して再利用できるようになります。
しかし状態を切り出しても、運び手がいなければ、その状態を複数コンポーネント間で共有することはできないのです。

以下のコード見ながら説明します。
複数コンポーネントでの説明のため、先程のカウンターアプリを、複数コンポーネントに分割します。

  • CounterApp(カウンターアプリの親コンテナ)
    • CounterDisplay(カウンターの値の表示)
    • CounterIncrementButton(カウンターのカウントアップボタン)
    • CounterDecrementButton(カウンターのカウントダウンボタン)
CounterDisplay.vue
<template>
  <div>{{ state.count }}</div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
import useCounter from "@/composables/use-counter"

export default defineComponent({
  setup() {
    const { state } = useCounter()
    return {
      state,
    }
  }
})
</script>

CounterIncrementButton.vue
<template>
  <button @click="increment">+</button>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
import useCounter from "@/composables/use-counter"

export default defineComponent({
  setup() {
    const { increment } = useCounter()
    return {
      increment,
    }
  }
})
</script>
CounterDecrementButton.vue
<template>
  <button @click="decrement">-</button>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
import useCounter from "@/composables/use-counter"

export default defineComponent({
  setup() {
    const { decrement } = useCounter()
    return {
      decrement,
    }
  }
})
</script>
CounterApp.vue
<template>
  <div>
    <CounterDisplay />
    <CounterIncrementButton />
    <CounterDecrementButton />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
import CounterDisplay from "@/components/CounterDisplay.vue"
import CounterIncrementButton from "@/components/CounterIncrementButton.vue"
import CounterDecrementButton from "@/components/CounterDecrementButton.vue"

export default defineComponent({
  components: {
    CounterDisplay,
    CounterIncrementButton,
    CounterDecrementButton,
  },
  setup() {
    return {}
  }
})
</script>

上記のコードは、残念ながら正常動作しません。
「+」を押してみても、カウントアップしません。
「-」を押してみても、カウントダウンしません。

スクリーンショット 2020-07-16 17.17.29.png

ボタンを押しても反応しなくなった(泣

何がダメなのでしょうか?

原因は、それぞれのコンポーネントでuseCouterをインポートしているのが原因です。
CounterDisplayとCounterIncrementButton、CounterDecrementButtonでは、別の状態がインポートされます。(インスタンスがコピーされるイメージ)

スクリーンショット 2020-07-17 17.44.11.png

つまり、それぞれの部屋に、ダンボールがあって中身は同じなのですが、すべてコピー品なのです。

コンポーネント間で、同じ状態を共有したい場合、ダンボールを一つ用意して、それを運ぶ必要があります。

スクリーンショット 2020-07-17 17.49.06.png

そう、この配達人こそProviderなのです!

nidai_hakobu_sagyouin_man.png

Providerを使えば、複雑な状態も簡単に管理することができます。

今後は、Composition APIとProviderを組み合わせて使うのがよいです。
Composition APIで「状態とロジックを切り出して」Providerで「運ぶ」って感じです。

nidai_hakobu_sagyouin_man_box.png

Providerを使うと、親コンポーネントから子コンポーネントに共通の状態を簡単に受け渡すことができます。
状態を渡す先は、子コンポーネントであればどこでも渡すことができます。
Prop渡しと違ってバケツリレーをする必要がありません。

スクリーンショット 2020-07-24 9.36.04.png

つまりVue3では「Provider」+「Composition API」の組み合わせで設計を考えていく必要があります。

Propsで渡す方法もありますが、コンポーネントツリーが深い場合、遠くのコンポーネントに運ぶのはバケツリレーがかなり大変です。。バケツリレーだと、荷物に関心のない人にも迷惑がかかるのでオススメしません。

「provide/inject」の使い方

提供する側(provide)と注入される側(inject)に分かれます。

  • 送信側(provide)
  • 受信側(inject)

実際に「provide/inject」を使ったコードを使って説明します。

まずは、UseCounterKeyを作ります。

これは、荷物にタグ付けするイメージです。

composables/use-counter-key.ts
import { InjectionKey } from '@vue/composition-api';
import { CounterStore } from './use-counter';

const CounterKey: InjectionKey<CounterStore> = Symbol('CounterStore');
export default CounterKey;

use-counterの戻り値について、型情報を追加しておきます。

composables/use-counter.ts
import { reactive } from "@vue/composition-api"

export default function useCounter() {
  // 状態
  const state = reactive<{
    count: number;
  }>({
    count: 0
  })

  // ロジック
  const increment = () => state.count++
  const decrement = () => state.count--

  return {
    state,
    increment,
    decrement,
  }
}

// 追加
export type CounterStore = ReturnType<typeof useCounter>

親コンポーネントであるCounterAppから、useCounterをkeyでタグ付けして、provideします。
住所を書いて、荷物の配達を依頼するイメージです。

CounterApp.vue
<template>
  <div>
    <CounterDisplay2 />
    <CounterIncrementButton2 />
    <CounterDecrementButton2 />
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from "@vue/composition-api"
import CounterDisplay2 from "@/components/CounterDisplay2.vue"
import CounterIncrementButton2 from "@/components/CounterIncrementButton2.vue"
import CounterDecrementButton2 from "@/components/CounterDecrementButton2.vue"
import CounterKey from "@/composables/use-counter-key"
import useCounter from "@/composables/use-counter"

export default defineComponent({
  components: {
    CounterDisplay2,
    CounterIncrementButton2,
    CounterDecrementButton2,
  },
  setup() {
    provide(CounterKey, useCounter())
    return {}
  }
})
</script>

続いて、荷物の受け取り側の処理です。
injectして、荷物を受け取ります。

CounterDisplay2.vue
<template>
  <div>{{ state.count }}</div>
</template>

<script lang="ts">
import { defineComponent, inject } from "@vue/composition-api"
import { CounterStore } from "@/composables/use-counter"
import CounterKey from "@/composables/use-counter-key"

export default defineComponent({
  setup() {
    const { state } = inject(CounterKey) as CounterStore
    return {
      state,
    }
  }
})
</script>
CounterIncrementButton2.vue
<template>
  <button @click="increment">+</button>
</template>

<script lang="ts">
import { defineComponent, inject } from "@vue/composition-api"
import { CounterStore } from "@/composables/use-counter"
import CounterKey from "@/composables/use-counter-key"

export default defineComponent({
  setup() {
    const { increment } = inject(CounterKey) as CounterStore
    return {
      increment,
    }
  }
})
</script>
CounterDecrementButton2.vue
<template>
  <button @click="decrement">-</button>
</template>

<script lang="ts">
import { defineComponent, inject } from "@vue/composition-api"
import { CounterStore } from "@/composables/use-counter"
import CounterKey from "@/composables/use-counter-key"

export default defineComponent({
  setup() {
    const { decrement } = inject(CounterKey) as CounterStore
    return {
      decrement,
    }
  }
})
</script>

スクリーンショット 2020-07-16 17.17.44.png

これで、ボタンをクリックして正常にカウントアップできるようになりました。

yubin_ukewatashi.png

以上が、Providerの説明でした。

今後、ProviderのAPIは変わるかも?

もともとPlugin開発やライブラリ開発向けのAPIとして「provide/inject」が用意されていました。
つまり、「provide/inject」はVue3用に用意されたAPIではないのです。
よって、Providerの仕組みは、今後、変わるかもしれません。
「provide/inject」仕組みは、問題点も多いからです。
例えばKeyを書き間違えたら不具合を引き起こしますが、それが実行時にしかわからないです。
injectの対象がコンポーネント単位のため、依存が大きいです。(もっと依存度を下げて使いたい)
今後Vue3で改善されると予想しています。

しかし、抽象レベルの「Providerパターン」の考え方は変わりませんので、知っておいて損はないかと思います。

まとめ

  • Composition APIを使って、散らかった部屋の荷物をダンボールに梱包して、外に出そう。
  • Composition APIを使って梱包した荷物を運ぶのがProviderの役目
  • Providerで、親コンポーネントから子コンポーネントに共通の状態を簡単に受け渡すことができる。
  • Prop渡しと違ってバケツリレーをする必要がない。
  • 今後、Vue3ではProviderの改善が進むと予想

最後まで読んでいただき、ありがとうございました!

参考リンク集

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
175
Help us understand the problem. What are the problem?