RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

NuxtアプリケーションでProvide・Injectを使ったStoreパターンを構成する

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の導入についても再度検討する予定です。

参考

お知らせ

Software Design 2020年11月号
今月号のFlutter特集ですが弊社所属のスーパーアプリエンジニアが執筆に参加されてます!!

非常にわかりやすい内容でこれからFlutterを触っていこうという方にうってつけの内容ですので 是非、手にとって内容確認して頂けると!!!