📷

Vue Composition API でprovide , inject を用いたデータの状態を管理する方法

2021/01/07に公開

この記事では、Vue Composition API で用意されているprovide, inject を使って、Vuex を用いないデータの状態管理について記事をまとめました。公式のドキュメントで詳しく記されていなかったので、この記事で詳しく書いていきたいと思います。

2020/2/26 追記:
今月2月に、Vue Composition API に対応した Vuex4 が公式に発表されました。
https://github.com/vuejs/vuex/releases/tag/v4.0.0

詳細についての記事は、こちらを参照してください。

Vuex について

Vuex は、Vue.js でグローバルでのデータの状態管理を行うことができるライブラリです。Vuex は、これまで propsemit を利用した親子間のコンポーネントでの状態管理の複雑さを解消するために用いられてきました。このライブラリにより、どのコンポーネントからでも状態にアクセスしたり、アクションを実行することができます。

Vuex を使用するにあたっての弊害

しかしながら、Vuex を使用するにあたって次の問題が挙げられました。

  • TypeScript との相性が悪い
  • アーキテクチャが複雑で手軽に扱うことが難しい
  • コンポーネントをテストを行うことが難しい

こういった課題を解決するために、Vue.js の2.2 以降に provideinject といったDI を行うことができる関数が追加されました。

Vue Composition API でも、provide, inject 関数が用意されており、このライブラリを利用してお手軽かつグローバルな状態管理を行うことができます。

Provide / Inject を用いて状態管理する方法

今回使うにあたり、使用したライブラリは以下の通りです。

  • nuxt : 2.0.0
  • @nuxt/typescript-build : 0,6,0
  • @vue/composition-api : 0.6.3

ここでは、Nuxt.js でプロジェクトの立ち上げ方やVue Composition API の 導入方法については省略します。

まず、Vue Composition API が使えるように設定します。

import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)

続いて、/compositions ディレクトリを用意して状態を定義します。

import { reactive, toRefs, InjectionKey } from '@vue/composition-api'

// interface で型を定義している
import { StateInterface, UserInterface } from '~/interfaces'

export const useGlobalState = () => {
  const globalState = reactive<StateInterface>({
    userState: {
      id: '',
      name: '',
      email: '',
    }
  })

  const setUserState = (stateValue: UserInterface) => {
    globalState.userState = stateValue
  }

  return {
    // toRefs を使って外部のファイルにエクスポートできるようにします。
    ...toRefs(globalState),
    setUserState
  }
}

type GlobalStateType = ReturnType<typeof useGlobalState>
export const GlobalStateKey: InjectionKey<GlobalStateType> = Symbol(
  'GlobalState'
)

このとき、InjectionKey を上記のように設定したのは、以下のような型が定義されているためです。


interface InjectionKey<T> extends Symbol {
}
declare function provide<T>(key: InjectionKey<T> | string, value: T): void;
declare function inject<T>(key: InjectionKey<T> | string): T | undefined;
declare function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T;

provide や inject のキーは string 型 で定義することもできますが、InjectKey<T> を利用して型を設定してあげることで、inject で注入された変数に型の注釈を与えることができます。

親コンポーネントで provide を設定

先ほど定義した状態をグローバルで使えるようにするために設定します。
Nuxt.js であれば、/layouts/default.vue にで設定を行います。

また、Vue composition API は setup() 内でしか使用することができないため、Nuxt.js の middleware で使用することができませんでした。今後この部分の仕様が随時変更されると思いますが、認証周りはこの/layouts/default.vue 内で定義したほうがいいと思います。

<template>
  // 省略
</template>

<script lang="ts">
// For Accounts Only

import {
  defineComponent,
  SetupContext,
  provide,
  inject
} from '@vue/composition-api'


import { UserInterface } from '~/interfaces'
import { OauthUserInterface } from '~/interfaces/user'
import { useGlobalState, GlobalStateKey } from '@/compositions'

export default defineComponent({
 
  setup(props: any, { root }: SetupContext) {
    provide(GlobalStateKey, useGlobalState())
    // 親コンポーネントでも inject を使って、状態の読み込み、書き込みを行うことができます。

    const state = inject(GlobalStateKey)
    if (!state) {
      throw new Error('NO Global Key')
    }
     // 次のようにしないと値が読み取れません。
    if (state.userState.value.id === '') {
      const parsedUserInfo: UserInterface = {
        id: '143562',
        name: 'example',
        email:'fuga@example.com',
      }
      state.setUserState(parsedUserInfo) 
    }
  }
})
</script>

上記のように、injectは、provide が定義された後であれば親コンポーネント内でも使用することができます。また、データの状態は、 'GlobalState' のキーを持つ provide 内のオブジェクトで管理されているため、値の読み込みや書き込みには、上記のコードのようにstate.setUserState() とする必要があります。

子コンポーネントでinject を利用してState を呼び出す

続いて、inject を利用して、先ほど親で定義した状態を子コンポーネントから呼び出し、利用できるようにします。

例えば、先ほどの個人情報を子コンポーネントである /pages/index.vue より呼び出します。

<template>
  <div>
   <v-row align="center" justify="center">
    <v-col cols="8" sm="9" xl="10">
      <v-card-title>
        {{ props.userName }}
      </v-card-title>
      <v-card-subtitle> @{{ props.userId }} </v-card-subtitle>
      <v-card-subtitle> @{{ props.email }} </v-card-subtitle>
    </v-col>
  </v-row>

  </div>
</template>
<script lang="ts">
import { defineComponent, inject } from '@vue/composition-api'
import { GlobalStateKey } from '@/compositions'

export default defineComponent({
  name: 'Settingwindow',
  
  setup() {
    const userState = inject(GlobalStateKey)
    if (!userState) {
      throw new Error(`${GlobalStateKey} is not provided`)
    }
    // userState.userState としないと値が読み取られません。
    console.log('state', userState.userState)
    return {
      userState: userState.userState
    }
  }
})
</script>

以上のようにして、Vuex を用いないでグローバルにデータの状態管理を行うことができました。

最後に

Vue Composition API では、確かに関数ベースでよりシンプルに状態管理を行うことができました。それでも、Vuex のようなFlux をベースとしたライブラリを利用すれば、よりクリーンなアーキテクチャを構築することができます。大規模な開発になっていくのであれば、Vuex を用いたものに書き換えていくほうがいいです。

次の記事のように、Nuxt.js と Composition APIでVuexのデータの状態をReactive に使う例もあります。

https://qiita.com/tubone/items/f5c7e8e79e21b051eec4

現在(2020/7/31)では、Vuex がVue Composition API に対応していませんが、今後対応されるのを期待しましょう。

参考文献

Discussion