はじめに
Composition APIで、**Providerパターン(provide/inject)**が、なぜ重要なのでしょうか。
Composition APIを使えば「状態やロジック」を簡単にコンポーネントの外に切り出すことができます。
しかしながら、Composition APIだけでは「ロジックや状態は切り出せても、複数コンポーネント間で共有できない」からです。
Composition APIによって、荷物をダンボールに梱包はできても、運ぶ人がいない状態です。
みなさんは、VueのProviderパターン(provide/inject)って使ったことありますか?
https://jp.vuejs.org/v2/api/index.html#provide-inject
「ん、Provider? なにそれ?」って感じですかね。
Providerパターンとは、**「荷物を運んで、受け渡す」**仕組みのことです。
Providerを使うと、親コンポーネントから子コンポーネントに共通の状態を簡単に受け渡すことができます。
状態を渡す先は、子コンポーネントであればどこでも渡すことができます。
Prop渡しと違ってバケツリレーをする必要がありません。
私は、Providerを使ったことありませんでした。
そもそも、存在すら知りませんでした。
私と同じように、Providerなんて、知らないし、使ったことない人が多いんじゃないかなと思います。
そこで、Providerの**「使い方」や「なぜ使うのか」**といった内容を、本記事にまとめてみようかと思います。
Remote.vueがきっかけでProviderを知る
先月行われた「Remote.vue #1」に、視聴枠で参加しました。
動画配信URL: https://youtu.be/rx249I9JmNU
来たるVue3時代を見据えた発表内容に、心躍らせました。
特に興味深かったのは、Vue3時代の設計についてフォーカスされていたセッションです。
登壇者の方から**「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を使えば、ロジックを再利用するために、コンポーネントから**「状態とロジック」を切り出す**ことができます。
ここからは、簡単なカウンターアプリのコードを見ながら説明します。
このカウンターアプリは、「+」を押すと+1カウントアップされ、「-」を押すと-1カウントダウンするという単純なものです。
「+」を押したら、カウントアップしたコンポーネントに「View、状態、ロジック」が混在した状態
上記のカウンターアプリを、VueのComposition APIを使ってSFCファイルに書くと、以下のようになります。
<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、状態、ロジック」**が混在しています。
コンポーネントが肥大化すると、可読性が下がったり、メンテナンスしにくくなったり、様々な問題を引き起こします。
そこで、コンポーネントの中から**「状態とロジック」を外に切り出し**ます。
Composition APIを使っているので、簡単に外に切り出すことができます。
「状態とロジック」を切り出したコードは、以下のようになります。
コンポーネントから、「状態とロジック」を外に切り出す
「useCounter」として、「状態とロジック」をコンポーネントの外に定義します。
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して、使うだけになります。
<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だけでは「ロジックや状態は切り出せても、共有できない」からです。
Composition APIによって、荷物をダンボールに梱包はできたのですが、運ぶ人がいない状態です。
ダンボールの中身を他の部屋の人が使いたいと思っても、使えません。
ダンボールに勝手に手足が生えて、勝手に配達先まで歩いていってはくれないのです。
Composition APIを使えば、「ロジックや状態」を切り出して再利用できるようになります。
しかし状態を切り出しても、運び手がいなければ、その状態を複数コンポーネント間で共有することはできないのです。
以下のコード見ながら説明します。
複数コンポーネントでの説明のため、先程のカウンターアプリを、複数コンポーネントに分割します。
- CounterApp(カウンターアプリの親コンテナ)
- CounterDisplay(カウンターの値の表示)
- CounterIncrementButton(カウンターのカウントアップボタン)
- CounterDecrementButton(カウンターのカウントダウンボタン)
<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>
<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>
<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>
<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>
上記のコードは、残念ながら正常動作しません。
「+」を押してみても、カウントアップしません。
「-」を押してみても、カウントダウンしません。
ボタンを押しても反応しなくなった(泣
何がダメなのでしょうか?
原因は、それぞれのコンポーネントでuseCouterをインポートしているのが原因です。
CounterDisplayとCounterIncrementButton、CounterDecrementButtonでは、別の状態がインポートされます。(インスタンスがコピーされるイメージ)
つまり、それぞれの部屋に、ダンボールがあって中身は同じなのですが、すべてコピー品なのです。
コンポーネント間で、同じ状態を共有したい場合、ダンボールを一つ用意して、それを運ぶ必要があります。
そう、この配達人こそProviderなのです!
Providerを使えば、複雑な状態も簡単に管理することができます。
今後は、Composition APIとProviderを組み合わせて使うのがよいです。
Composition APIで「状態とロジックを切り出して」Providerで「運ぶ」って感じです。
Providerを使うと、親コンポーネントから子コンポーネントに共通の状態を簡単に受け渡すことができます。
状態を渡す先は、子コンポーネントであればどこでも渡すことができます。
Prop渡しと違ってバケツリレーをする必要がありません。
つまりVue3では**「Provider」+「Composition API」の組み合わせで設計を考えていく必要**があります。
Propsで渡す方法もありますが、コンポーネントツリーが深い場合、遠くのコンポーネントに運ぶのはバケツリレーがかなり大変です。。バケツリレーだと、荷物に関心のない人にも迷惑がかかるのでオススメしません。
##「provide/inject」の使い方
提供する側(provide)と注入される側(inject)に分かれます。
- 送信側(provide)
- 受信側(inject)
実際に「provide/inject」を使ったコードを使って説明します。
まずは、UseCounterKeyを作ります。
これは、荷物にタグ付けするイメージです。
import { InjectionKey } from '@vue/composition-api';
import { CounterStore } from './use-counter';
const CounterKey: InjectionKey<CounterStore> = Symbol('CounterStore');
export default CounterKey;
use-counterの戻り値について、型情報を追加しておきます。
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します。
住所を書いて、荷物の配達を依頼するイメージです。
<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して、荷物を受け取ります。
<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>
<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>
<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>
これで、ボタンをクリックして正常にカウントアップできるようになりました。
以上が、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の改善が進むと予想
最後まで読んでいただき、ありがとうございました!
参考リンク集
-
終わりゆく Vue 2.x 時代の状態設計の答えと Vue 3 の Provider への期待
https://speakerdeck.com/potato4d/the-last-architecture-of-the-vue-2-dot-x -
ぼくのかんがえたさいきょうのVueあーきてくちゃ
https://speakerdeck.com/slont/bokufalsekangaetasaikiyoufalsevueakitekutiya -
Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか?
https://qiita.com/tmy/items/a545e44100247c364a71 -
Vue2 API provide/inject
https://jp.vuejs.org/v2/api/index.html#provide-inject -
Vue Composition APIでストアパターンをスマートに使って状態管理をする
https://qiita.com/resessh/items/ab09ec925ca49d02caae -
Vue Composition API + TypeScriptで DI(依存性の注入), DIP(依存性逆転の原則) を実装してみる
https://qiita.com/ryo2132/items/03380df2df5b4b2933d7