【Vue3】Provide / Inject の書き方 ~Vuexを使わないグローバルな状態管理~

  • このエントリーをはてなブックマークに追加
  • LINEで送る

こんにちは!りょた(@Ryo54388667)といいます(^o^)

今回は、本業の方で少しVue3を触ったので、備忘録がてら、まとめていきます。

  • Vue3の状態管理を知りたい。
  • Vue3の書き方を知りたい。
  • 実務で使いそうだから、すぐに習得する必要がある。

そのような方に読んでいただけると幸いです(^^)

コードは全てこちらのページにあげています。

1 作成するアプリのイメージ

今回はボタンを押すと数字が上下するアプリを作成します。

Vuexを用いる場合と比較するとわかりやすいかと思いますので、Vuexを用いる場合の構成を書いていきます。

Vuexを用いる場合

store/index.ts

stateオブジェクトの中に

count: 0

のような初期値を格納する。

mutationオブジェクトの中に

increment(state) => void
decrement(state) => void

のようなcountアップとダウンのメソッドを準備する。

あとはgettersオブジェクトに呼び出しもとで用いる、getterメソッドを準備すれば、各コンポーネントから呼び出しができ、

グローバルな状態管理が実現できます。

これはVuexを用いたグローバルな状態管理です。

この考え方と比較しながらprovide / inject の方法を見るとよいかと思います。

では本題の方にいきましょう(^^)



2 Provide /  Inject  の書き方

まずはVuexでいうStoreを作成していきます。

tsファイルを作成していきます。

srcディレクトリ直下に「providers」ディレクトリを作成してください。

その中にtsファイルを作成します。名前は任意のもので構いません。

ここで少し、小話ですが、Vue3はどことなく、Reactに似ています。

Reactで言えば、providersディレクトリの中には「use〇〇Provider.ts」というふうに名前をつけるので、

Vue3のprovidersディレクトリにも「use〇〇Provider.ts」というふうに名前をつけるとよいと思います。

僕のGitHubはそうなっていませんが、そうするとよかったと、たった今思いました(^^;

話が少し外れました。

続いて、tsファイルの中を記述していきます。

◯ state,  actionの設定

type State = {
  count: number;
};

まず、stateの型を定義します。

カンマではなく、セミコロン区切りということに注意ですね。

この型定義が結構よい!

Vuexでは行わないことなので、はじめは何をしているかわかりませんが、型を定義することで、実際にstateのプロパティを入力する時に、

型の補完が効くようになります。

これがすごくイイ!

Vuexで何度スペルミスで値がundefindeになったか、数え切れません。。。僕だけかもしれませんが(^^;

stateの中を追加するときは、同時に型のにも追記するのを忘れてはいけません。

例えば、Stateに、countプロパティの他に、messageプロパティを追加したい場合は、

type State = {

count: number;

message: string;

}

というふうに追記する必要があります。

続いて、storeの中を書いていきます。

とりあえず、全体像を置いておきます。

export const useStore = () => {
  //state
  const globalState = reactive<State>({
    count: 0,
  });

  //action
  const increment = () => {
    globalState.count++;
    console.log("Store内 increment", globalState.count);
  };
  const decrement = () => {
    globalState.count--;
    console.log("Store内 decrement", globalState.count);
  };
  
  return { ...toRefs(globalState), increment, decrement };
};

順番に解説していきます。

まず、Stateですね。

//state
  const globalState = reactive<State>({
    count: 0,
  });

このように書きます。

見慣れないのは、このreactive<State>部分ですね。

reactiveはVue3で新たに追加された機能です。

これがないと、呼び出しもとのコンポーネントのtemplateタグ内で変更が反映されません。

そして、<State>は、後に続くオブジェクトの型を示しています。

この記述で、プロパティ部分で型補完が効くようになります。

続いて、actionですね。

Vuexのときとは異なり、actionもmutationも区別がなくなりました。

つまり、stateの更新に関して、非同期の関与の有無は気にしなくてよくなったということです。

また、gettersも無くなりました。コンポーネント側から直接、actionやmutationを呼び出します。

//action
  const increment = () => {
    globalState.count++;
    console.log("Store内 increment", globalState.count);
  };
  const decrement = () => {
    globalState.count--;
    console.log("Store内 decrement", globalState.count);
  };

メソッドの書き方は、アロー関数が推奨されています。

以前のクラス内のメソッドの書き方ではなく、一般的なJavaScriptのメソッドの書き方ですね。

exportするstoreのメソッドの最後に必ず、returnが必要です。

return { ...toRefs(globalState), increment, decrement };

何をreturnするかというと、stateとactionを全てをオブジェクト内に格納し、そのオブジェクトをreturnします。

その際、見慣れないのは…toRefs()ですね。

これは、端的にいうと、()内のオブジェクトのプロパティをref付きでバラバラにするイメージです。

モダンJavaScriptを学習した人に説明するなら、分割代入をしたプロパティがrefオブジェクトになっているような感じです。

コンポーネント側で呼び出す場合、

globalState.countではなくcountと省略できます。しかも、countの値を確認すると、

console.log(count)  //ref: { value:  0 }

のようにrefオブジェクトになっています。これによって、templateタグ内でも変更が反映されるようになります。



◯ keyの設定

終わりかと思いきや、まだtsファイルの設定です。

先ほどのstateやactionが記述された、storeメソッドの外に、外部からアクセスするための設定を記述します。

//親,子コンポーネントで用いるkey。return {  }の型の表現が33行目。
type storeType = ReturnType<typeof useStore>;
export const storeKey: InjectionKey<storeType> = Symbol("store");

「お見初めいたすっ!」

という感じではないでしょうか。

僕は初めて見た時、見慣れないというか、コードの役割を全く想像できませんでした。

1行目は、typeからわかるように、型を定義しています。

storeメソッドそのものの型を定義しています。

ReturnTypeという型をvue側が用意しているので、これをインポートし、return {  }このオブジェクトの型を定義します。

typeof で型を推測させ、型を定義します。

そして、最後にkeyの設定です。

これは、コンポーネント側から、このStoreにアクセスする時に必要なものです。なので、exportも必要です。

export const storeKey: InjectionKey<storeType> = Symbol("store");

この部分のベストプラクティスはまだ定かではないのですが、

個人的には、これがよいかなと思ったものを記述します。

InjectionKeyという型をvueからインポートします。その型に<>の中で、先ほどの一行で定義したstoreメソッドの型を書きます。

これによって、storeキーはstoreメソッドを利用する際のInjectのキーであることが認識されます。

右辺にはSymbolとありますが、これはJavaScriptから提供されているメソッドです。

Keyを暗号化するものです。なので、console.log()で確認しても、中は分かりません。

一応、tsファイル全体掲載しておきます。

import { InjectionKey, reactive, toRefs } from "vue";


type State = {
  count: number;
};

export const useStore = () => {
  //state
  const globalState = reactive<State>({
    count: 0,
  });

  //action
  const increment = () => {
    globalState.count++;
    console.log("Store内 increment", globalState.count);
  };
  const decrement = () => {
    globalState.count--;
    console.log("Store内 decrement", globalState.count);
  };

  return { ...toRefs(globalState), increment, decrement };
};

//親,子コンポーネントで用いるkey。return {  }の型の表現が33行目。
type storeType = ReturnType<typeof useStore>;
export const storeKey: InjectionKey<storeType> = Symbol("store");

これで、tsファイルの設定は終わりです。

これで終わりではありません。

まだ他のコンポーネントから呼び出しても、storeを利用することはできません。

◯ provideの設定

ようやくタイトルの一つであるprovideが出てきます。

ここで一つのVueファイルを作成します。

これですが、GitHubにはprovidersディレクトリに内に作成してありますが、src直下の方がよりよいかなと今現在思っております。

作成する場所はどこでも問題ありません。

さて、コードを見ていきましょう!

vueファイル

<template>
  <div>
    <slot />
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from "vue";
import { useStore, storeKey } from "@/provider/StoreProvider";
export default defineComponent({
  //setupの中にフィールド変数もメソッドも記入。
  setup() {
    //基本的にはルートコンポーネントでprovideをかく。
    //親コンポーネントで必要。第一引数はkey, 第二引数はStore。
    provide(storeKey, useStore());
    return {};
  },
});
</script>

scriptタグ内を見てください。

実はproviderの設定はいたってシンプルです。

provideをVueからインポートし、同時に、先ほど設定したKeyをtsファイルからインポートします。

あとはsetup関数内でprovideメソッドを呼び出すだけです。

provide(  第一引数はKeyが入る,  第二引数はstore本体のメソッドが入るuse○○() );

この一行でscriptタグ内の設定は完了です。

次に、templateタグを見てください。

ここは<slot />を書くだけです。

<template>
  <div>
    <slot />
  </div>
</template>

これはVueが提供する機能の一つです。

ちなみにVue2系にもありました。

Reactを学習した人はchildrenの替わりといった方が分かりやすいかもしれません。

簡単に<slot />を説明しますね。

今作成したVueファイルをprovide.vueとします。

このVueファイルをコンポーネントとして別のVueファイルで利用するとき、

<Provide><img src=“” /></Provide>

このような形で、タグで挟み込んだとき、

provide.vueファイルのtemplateタグ内の<slot />に<img src=“” />が入り込むイメージです。

端的に言えば、挟み込まれたものが<slot />部分に置き換わる、みたいな役割です。

この役割を踏まえると、

provideの設定したものが<slot />で置き換わったものに適応されることになります。

このようにしてprovideを設定していきます。

あとはこのprovideの設定を「全てのページ」で適応させたいですね。

全てのページで適応させるには、

<provide> <  ????   ></ provide>

のように何を挟めばよいのでしょうか。

Appコンポーネント内で利用する方法ですね。

Reactを学習した人なら、容易に想像できる部分でしょう。

結論からいうと、

App.vue内の<router-view />を挟みましょう。

<template>
  <div id="nav">
    <router-link to="/">Count</router-link>
  </div>
  <!-- providerをルートコンポーネントで囲う(slot部分) -->
  <StoreProvider>
    <router-view />
  </StoreProvider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import StoreProvider from "./provider/StoreProvider.vue";
export default defineComponent({
  components: {
    StoreProvider,
  },
});
</script>

router-viewは全てのページがマウントされている部分なので、ここを挟み込んでおけば、

全てのページでprovideの設定が適応されることになります。

App.vueのscriptタグ内でcomponentsプロパティ内にimportしたコンポーネントを設定するのを忘れずに!

あとは子コンポーネントで呼び出すだけです!

結構大変かと思いますが、もう少しです(^^)



◯ Injectの設定

最後の最後に2つ目のキーワードInjectが出てきましたね。

このInjectの設定は簡単です。

Inject( 設定したKeyが入る )

これだけでstoreを利用することができます(^^)

<template>
  <div>
    <h4>カウントエリア</h4>
    <button v-on:click="onDecrement">-</button>
    <span>countの値: {{ store.count.value }} </span>
    <button v-on:click="onIncrement">+</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";
import { storeKey } from "@/provider/StoreProvider";
export default defineComponent({
  //setupの中にフィールド変数もメソッドも記入。
  setup() {
    //injectにはexportしたキーを入れる。useStoreの中のstate,actionが使える。
    const store = inject(storeKey);

    //storeのエラーを回避。undefindeの可能性を潰すため。
    if(!store){
      throw new Error("")
    }

    const onIncrement = () => {
      store.increment();
    };
    const onDecrement = () => {
      store.decrement();
    };

    return { store, onIncrement, onDecrement };
  },
});
</script>

injectをvueからインポートして、

const store = inject(key)

とするだけで、store内のstateやactionを利用することができます。

これの便利なところはVScodeの補完が効くところです。

Vue2系のときは補完が効きませんでした(^^;

「スペルミスが原因のエラーかよ。。。。」

のようなことがなくなります。

ここでちょっとTipsがあります。

コードを見ると、

if( !store ){
 throw new Error(“”)
}

のコードがありますね。

これにも役割があります。

試しにこの三行を消して、store.stateのように書いてみてください。

VScodeの補完機能で、

store?.state

のように修正されると思います。?が現れます。

この原因はstoreがundefindeの可能性があるということです。

設定をしたので、絶対存在するのですが、「絶対存在する」とはVScodeは認識していないんですね。

そこで、undefindeの可能性を潰すために、上記の三行があります。

三行の意味としては、

「もし、storeが存在しなければ、Errorを返す。」

というものです。

ですので、言い換えると、それ以降のコードではstoreは存在することが前提になるので、undefindeの可能性を潰すことができています。

こうすれば、意図通り、

store.state

のように記述できると思います!

あとは、scriptタグ内のreturn {  }の中にstoreを入れてあげれば、

templateタグ内でも利用することができます。

return { store, onIncrement, onDecrement };

お疲れ様でした!これで全ての設定が終わりました。

storeを利用するときは、呼び出しもとの子コンポーネントでその都度、Injectします。

こんな形でVuexを利用しない、グローバルな状態管理を行います。

全てのコードはこちらです。



3 最後に

いかがでしょうか。はじめは設定が結構大変に感じると思いますが、一度設定してしまえば、ラクになるかと思います。

何度も言いますが、この型補完が効くのがサイコーなんですよね。

ぜひ、挑戦してみてください(^^)

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

未経験からエンジニアに転職するまでにやってきたことについては下の記事にすべて書きました。

あわせて見てみてください(^^)

  • このエントリーをはてなブックマークに追加
  • LINEで送る