Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Vue3.0における状態管理(Vuex,Provide / inject)

こんにちは、フロントエンドエンジニアの小玉です。

今回は、Vue.jsにおける状態管理についてお話したいと思います。

Vue2.x時代、多くの方はVuexを使用していたのではないでしょうか?Vue2.x、Vue3.xともに公式のドキュメントにおいても、状態管理のセクションでVuexが紹介されています。
ただVue3.0以降は、Vuexに打って変わる状態管理方法がいくつか提唱されていますので、それぞれの内容を簡単に見つつ比較したいと思います。

Vuex

VuexVue.jsのアプリケーションのための状態管理ライブラリであり、単純なグローバルオブジェクトとは異なり、Vuexのストアはリアクティブです。
また、Vuexは以下のように単方向のデータフローになるので、変更される状態の追跡が明示的です。
vue-devtoolsを使用すると、getterで得られる値、mutationcommitした履歴などが確認できます。

vuex.png

vuex.vuejs.org

actions

  • stateを直接更新することはなく、mutationsを経由することでstateを更新する。
  • 非同期の処理を入れることができる。

mutations

  • actionsから受け取った値をstateにセット

mutationsでは単にstateの値を変更するだけの処理を行うことが多いかと思います。
それに対しactionsでは比較的複雑なロジックを記述しmutationsに対して値を返すイメージです。

実際のコード

今回は簡単なカウンターアプリを実装します。
SFC内部で全て完結させる場合は上記のような形になります。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import { reactive, computed, defineComponent } from "vue";
export default defineComponent({
  name: "App",
  setup() {
    const state = reactive({
      count: 0,
    });
    const count = computed(() => state.count);
    return {
      count,
      increment() {
        state.count += 1;
      },
      decrement() {
        state.count -= 1;
      },
    };
  },
});
</script>

では、実際にVuexを使用し、コンポーネントを分割した場合はどのような記述になるでしょうか。
ディレクトリ構成は以下のような形で進めてみます。

/src
 ├ components
 │  ├ DecrementBtn.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.js
 └ App.vue

Vuexのストアの定義を以下のようにします。

▼store/index.js

import { createStore } from "vuex";

const state = {
  count: 0
};
const mutations = {
  increment(state) {
    state.count++;
  },
  decrement(state) {
    state.count--;
  }
};
const actions = {
  increment: ({ commit }) => commit("increment"),
  decrement: ({ commit }) => commit("decrement")
};

export default createStore({
  state,
  actions,
  mutations
});

▼親コンポーネント(App.vue)

<template>
  <div>
    <p>{{ count }}</p>
    <increment-btn />
    <decrement-btn />
  </div>
</template>

<script lang="ts">
import { computed, defineComponent } from "vue";
import { useStore } from "vuex";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    const counter = useStore();
    const count = computed(() => counter.state.count);

    return {
      count,
    };
  },
});
</script>

▼子コンポーネント(DecrementBtn.vue)

<template>
  <button @click="decrement">+</button>
</template>
<script>
import { defineComponent } from "vue";
import { useStore } from "vuex";

export default defineComponent({
  setup() {
    const counter = useStore();
    return {
      decrement: () => counter.dispatch("decrement"),
    };
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

簡単なアプリケーションということもありますが、コンポーネント間のストアへのアクセス方法や、データフローなど非常に明示的であることがわかるかと思います。

Provide / inject

Provide / injectは、これまでコンポーネントの階層の深さに関係なく、親コンポーネントから子階層へ依存関係を提供するプロバイダとして機能します。
これまでは親コンポーネントから子コンポーネントにデータを渡す際、propsを使用していたかと思います。
しかし、階層が深くなるにつれバケツリレーのように伝搬していくのは非常に面倒でした。

components_provide.png

v3.ja.vuejs.org

こちらはVue2.2から実装されているものでしたが、Vue3.0からはComposition APIのリリースに伴い注目されています。

Composition API

Composition APIは、ロジックをカプセル化することでコンポーネント間でのロジックの再利用を可能にするAPIです。
特に大規模なアプリケーションになると、Vue2.xまではViewとロジックが密結合になっており、SFCが冗長になってしまうことが問題でした。
しかし、Composition APIを活用することでSFCにはViewに関することのみを記述し、ロジックを外部に切り出すようなことも可能になります。

実際のコード

基本的なディレクトリ構成は変わらず、以下のような形。

/src
 ├ components
 │  ├ CountView.vue
 │  ├ DecrementBtn.vue
 │  ├ ParentProvider.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.ts
 │  └ key.ts
 └ App.vue

コンポーネント内でしていた状態管理の部分を切り抜き、ストアを作成しキーを定義します。

▼index.ts

import { reactive } from "vue";
export default function counterStore() {
  const state = reactive({
    count: 0
  });
  return {
    get count() {
      return state.count;
    },
    increment() {
      state.count += 1;
    },
    decrement() {
      state.count -= 1;
    }
  };
}
export type CounterStore = ReturnType<typeof counterStore>;

▼key.ts

import { InjectionKey } from "vue";
import { CounterStore } from "./index";

const CounterKey: InjectionKey<CounterStore> = Symbol("counterStore");
export default CounterKey;

provideは( key , value )を受け取り、provideされた値はコンポーネントからキーを用いてinjectを取り出せます。

▼親コンポーネント(ParentProvider.vue)

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

<script lang="ts">
import { provide } from "vue";
import counterStore from "../store/index";
import CounterKey from "../store/key";

export default {
  setup() {
    provide(CounterKey, counterStore());
    return {};
  },
};
</script>

これでParentProvider.vue以下のコンポーネントはどの階層においてもストアオブジェクトを受け取ることが可能です。

▼子コンポーネント(CountView.vue)

<template>
  <p>{{ count }}</p>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
import CounterKey from "../store/key";

export default defineComponent({
  setup() {
    const counter = inject(CounterKey);
    const count = computed(() => counter.count);
    return {
      count,
    };
  },
});
</script>

ストアオブジェクトを受け取る側はinjectにキーを指定します。同様の記述でボタン関係も実装します。
最終的には以下のようにまとめることでVuexやSFC1つで記述していたものと同様の動きをします。

▼App.vue

<template>
  <div>
    <parent-provider>
      <count-view />
      <increment-btn />
      <decrement-btn />
    </parent-provider>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ParentProvider from "./components/ParentProvider.vue";
import CountView from "./components/CountView.vue";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    ParentProvider,
    CountView,
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    return {};
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

まとめ

provideは、今回のようなルートコンポーネント以外のコンポーネントからも使用することができます。
つまり、限定的な範囲でストアを定義することが可能となります。
Vuexではグローバルに定義する必要があったため、プロジェクトの規模によってはスコープの小さい状態管理ができるProvide / injectを使用するのもいいかもしれません。
一方で、Vuexは、Fluxでデータフローなどが決まっているため、チーム内でのガイドライン等を作る手間が少なく、さらにDevToolでのデバックができることも大きな利点かと思います。
チームとしてComposition APIを使用し、かつ規模の小さいプロジェクトの場合Provide / injectを使用し、大規模なプロジェクトの場合Vuexを使う。そんな選択が今後できるかと思います。

参考にさせていただいた記事等


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください↓

【フロントエンドエンジニア】
場所にとらわれず自社メディア成長に貢献したいフロントエンドエンジニア募集! - 株式会社WizのWebエンジニアの求人 - Wantedly

【バックエンドエンジニア】
勤務地自宅を叶える!バックエンドエンジニアとして事業を成長させたい方募集 - 株式会社WizのWebエンジニアの求人 - Wantedly