APP/UXチームに所属しております、フロントエンドエンジニアの田原です。
今回はComposition Functionを使うに際に便利なProvide・Injectの機能を使った Storeパターンについてご紹介したいと思います。
目次
はじめに
Composition FunctionはVue3.x系から正式に機能として組み込まれたCompositionAPIを利用した際の関数の総称を言い、ロジックをComponentから引き剥がす事ができ非常に見通しが良くなる便利なものです。
通常は読み込まれる(関数をimportした)Componentと対になる関係であり Componentを横断し共通の値の変更を検知することはできません。
今回はサンプルとしてカウントアップ・ダウンするComposition FunctionであるuseCounter.tsを用意し
その処理について表示部と処理部についてComponentを分割した形で以下に例を記載しております。
通常の利用について
- SampleComponentにuseCounter.tsをimportする
- SampleComponent内でreactiveな値(例でいうcount)や関数を表示に利用する
e.g. useCounter.ts
import { reactive, computed } from '@nuxtjs/composition-api' const useCounter = () => { const state = reactive({ count: 0 }) const count = computed(() => state.count) const increment = () => state.count++ const decrement = () => state.count-- return { count, increment, decrement, } } export default useCounter
e.g. SampleComponent
<template> <div> <div>{{ count }}</div> <button @click="increment">+</button> <button @click="decrement">-</button> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import useCounter from '~/composition/useCounter' export default defineComponent({ setup() { const { count, increment, decrement } = useCounter() return { count, increment, decrement } } }) </script>
上記の様にComponent内で処理(加算と減算)と表示(count)が共存しているComponentであれば特に問題はないのですが、パーツの再利用性なども考慮すると加算ボタン・減算ボタンと表示部を分けたComponentにしたいということがあるかと思います。
Componentを横断して共通の値を参照したい場合
- useCounter.tsは先程と同様
- 表示部・加算・減算を各Componentに分割
- 各ComponentにComposition Functionをimportしてもreactiveな値や関数の発火は共有されない
→ その為、処理が走りません
これを解決するためにProvide・Injectを利用します。
e.g. SampleCountComponent
<template> <div> <div>{{ count }}</div> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import useCounter from '~/composition/useCounter' export default defineComponent({ setup() { const { count } = useCounter() return { count } } }) </script>
e.g. SampleAddComponent
<template> <div> <button @click="increment">+</button> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import useCounter from '~/composition/useCounter' export default defineComponent({ setup() { const { increment } = useCounter() return { increment } } }) </script>
e.g. SampleSubComponent
<template> <div> <button @click="decrement">-</button> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import useCounter from '~/composition/useCounter' export default defineComponent({ setup() { const { decrement } = useCounter() return { decrement } } }) </script>
e.g. SampleMergeComponent
<template> <div> <SampleCount /> <SampleAddButton /> <SampleSubButton /> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import SampleCount from '~/components/atoms/Sample/SampleCountComponent.vue' import SampleAddButton from '~/components/atoms/Sample/SampleAddComponent.vue' import SampleSubButton from '~/components/atoms/Sample/SampleSubComponent.vue' export default defineComponent({ components: { SampleCount, SampleAddButton, SampleSubButton } }) </script>
ここまでを準備すると表示部・加算部・減算部に別れたComponentが合わさった表示が見えるようになりますが 各配下Component(SampleCount, SampleAddButton, SampleSubButton)で各自useCounter.tsをimportしているので値の連携が取れず、処理が正常に動作しません。 ※各自importのタイミングでComposition Functionがインスタンス化され、全く別のインスタンスとして扱われるイメージです。
これをProvide・Injectを使って書き換えます。 ※TypeScriptでの例になります
- まずインテリセンスを効かせたい為、Keyを作ります
- useCounter.tsのexport typeにも型を設定します
- WrapperのComponentを用意して配下Componentをslotでラップし、Provideを行います
- 読み込んだComposition Functionを利用したい配下Componentで呼び出してinjectします
- これでReactiveな値や関数がComponentを横断して共有することができるようになります
e.g. Composition Key
/* eslint-disable import/named NuxtのCompositionAPIの場合書かないとerrorになる為*/ import { InjectionKey } from '@nuxtjs/composition-api' // ↓これはuseCounterで型をexportしたものです import { CounterStore } from '~/composition/useCounter' export const CounterKey: InjectionKey<CounterStore> = Symbol('CounterStore')
e.g. useCounter.ts
export type CounterStore = ReturnType<typeof useCounter> //この行を追加 export default useCounter
e.g. SampleProviderComponent
これがComposition Functionを配下で利用できるようにする為のProviderComponentになります
<template> <div> <slot /> </div> </template> <script lang="ts"> import { defineComponent, provide } from '@nuxtjs/composition-api' import { CounterKey } from '~/compositionKey/useProvideKey' import useCounter from '~/composition/useCounter' export default defineComponent({ setup() { provide(CounterKey, useCounter($environments)) return {} } }) </script>
e.g. SamplePage
先程のProviderComponentの間にSampleMergeComponentを読み込みます。
※Componentの粒度としてはコレが一番外側のComponentになります。
Nuxtで言うところのPages等にコレを設定するとPage単位での値の共有が可能です。
<template> <SampleProvide> <SampleProvideTemplate /> </SampleProvide> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import SampleProvide from '~/components/provider/sampleProvide/index.vue' import SampleMergeComponent from '~/components/templates/sampleProvide/index.vue' export default defineComponent({ components: { SampleProvide, SampleMergeComponent } }) </script>
e.g. SampleCountComponent
<template> <div> <div>{{ count }}</div> </div> </template> <script lang="ts"> import { defineComponent, inject } from '@nuxtjs/composition-api' import { CounterStore } from '~/composition/useCounter' import { CounterKey } from '~/compositionKey/useProvideKey' export default defineComponent({ setup() { const { count } =inject(CounterKey) as CounterStore return { count } } }) </script>
e.g. SampleAddComponent
<template> <div> <button @click="increment">+</button> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import { CounterStore } from '~/composition/useCounter' import { CounterKey } from '~/compositionKey/useProvideKey' export default defineComponent({ setup() { const { increment } =inject(CounterKey) as CounterStore return { increment } } }) </script>
e.g. SampleSubComponent
<template> <div> <button @click="decrement">-</button> </div> </template> <script lang="ts"> import { defineComponent } from '@nuxtjs/composition-api' import { CounterStore } from '~/composition/useCounter' import { CounterKey } from '~/compositionKey/useProvideKey' export default defineComponent({ setup() { const { decrement } =inject(CounterKey) as CounterStore return { decrement } } }) </script>
Globalで使いたい場合
Page単位での値の共有については上記の通りですが全ての配下Componentで横断して Globalな使い方をしたい場合はNuxtの場合layerの層(default.vueなど)、Vueの場合App.ts等にProvideを行えば 配下での共有が可能になります。
結果
Provide・Injectを利用することにより、兄弟間のComponentに対してはprops drilling問題について気にすることなく 横断してstate&Functionを共有することができます。
まとめ
Composition Functionの利点として読み込まれるComponentに閉じたカプセル化を行える事でVuexのように 1つのglobalなデータを扱うというデメリットは抑えられる為、この方法を採用しました。 大規模アプリケーションでの実装においてはVuexの利点も大きいですが、各Storeに対してのアクセスをルール化する等 一定の秩序を持たせなければならず実装上、煩雑になる可能性が高いことが多い為、Vuexの利用を一時的に止めこの方法を利用しております。 開発中のアプリケーションが大きくなるにつれて、扱い難くなればVuexの導入についても再度検討する予定です。
参考
- vueschool.io
- Vue3 Composition APIにおいて、Providerパターン(provide/inject)の使い方と、なぜ重要なのか、理解する。
- 終わりゆく Vue 2.x 時代の状態設計のアンサー
お知らせ
Software Design 2020年11月号
今月号のFlutter特集ですが弊社所属のスーパーアプリエンジニアが執筆に参加されてます!!
非常にわかりやすい内容でこれからFlutterを触っていこうという方にうってつけの内容ですので 是非、手にとって内容確認して頂けると!!!