この記事はVuex5 RFC Proposalを参考にした内容です。正式リリース時には変更されている部分があるかもしれませんし、私自身も実際に触ってみて意見が変わる可能性があります。

Vuexの役割のおさらい

this RFC focuses on making Vuex an official global state management tool for Vue

Vuexには、親子関係にないVueComponent間で状態を共有する機能、つまりグローバルな状態管理機能を公式に提供する役割があります。今まではその実現手段としてFluxアーキテクチャをベースにしていましたが、Vuex5では脱却するようです。

また、グローバルな状態管理に加えて、Vuexが持つべき要件が4つ定義されています。

  • Code Splitting
  • SSR 対応
  • Vue Devtools 対応
  • Vuex を利用した状態管理の拡張性

私がVuexに期待することもこの要件の通りなので、今後のリリースを楽しみに待ちたいと思います。

ちなみにFluxには随分お世話になったし好みに合う考え方でした。時代とともに設計方法も進化していくもので今後利用頻度は減るかもしれませんが、頭の片隅に残っていてくれると、いつかどこかでシナジーが生まれるかもしれません。その日まで少しのお別れです。

Vuex 5 の簡単なチュートリアル

  1. Storeの定義
  2. Storeのインスタンス化
  3. ComponentからStoreへのアクセス

の順でRFCにて紹介されています。

1. Storeの定義

Vuex5ではStoreの定義方法が2種類用意されています。Option StoreComposition Store です。Vueで用意されているOptions API, Composition APIに寄せた手法なので馴染みやすそうです。

Option Store
import { defineStore } from 'vuex'

const useCounter = defineStore({
  key: 'counter',

  state: () => ({
    count: 1
  }),

  getters: {
    double() {
      return this.count * 2
    }
  },

  actions: {
    increment() {
      this.count++
    }
  }
})

defineStoreにstate, getters, actions等を含むオブジェクトを渡す形です。今までのVuexで書き慣れたプロパティなので分かりやすいですが、stateが関数になっている点は要注意です。mutationsは廃止されるようです。

Composition Store
import { ref, computed } from 'vue'
import { defineStore } from 'vuex'

const useCounter = defineStore('counter', () => {
  const count = ref(1)

  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return {
    count,
    double,
    increment
  }
})

defineStoreにkeyとsetup関数を渡す形式です。VueのrefcomputedなどのReactivity APIを使い回しているのが特徴的です。

2. Storeのインスタンス化

ここまでで定義してきたStoreは、Vuexインスタンスにregisterされることでインスタンス化されます。
vuex.store関数に先程定義したuseCounterを渡しましょう。

import { createVuex } from 'vuex'
import { useCounter } from '@/stores/counter'  // Option Store or Composition Store

const vuex = createVuex()

const counter = vuex.store(useCounter)

counter.count  // <- 1
counter.double // <- 2
counter.increment() // success

インスタンス生成後は、gettersdispatchなどを使わずともStoreにアクセスできるようになりました。今までのVuexより直感的です。

Composition Storeではrefを利用していてるのでcounter.count.valueとしたいとことですが、その必要はないようです。Option StoreかCompositon Storeかを区別することなく実装できるのはいいですね。

また、vuex.store(useCounter)は初回実行時にStoreインスタンスを内部的に保持し、2回目以降の実行時にはそれを返す仕組みがあります。言い換えるとStoreは使用されるまで登録されないということになります。これはCode Splittingの観点でも恩恵があります。

3. ComponentからStoreへのアクセス

ComponentがStoreを利用するためには、何はともあれ最初にVueアプリケーションにVuexインスタンスをinjectする必要があります。

import { createApp } from 'vue'
import { createVuex } from 'vuex'
import App from '@/App.vue'

const app = createApp(App)

const vuex = createVuex()

app.use(vuex)

app.mount('#app')

準備ができたところで、Options APIで実装されたComponentからのアクセス方法を見てみます。

方法1

import { useCounter } from '@/stores/counter'

export default {
  computed: {
    counter () {
      return this.$vuex.store(useCounter)
    },

    count() {
      return this.counter.count
    },

    double() {
      return this.counter.double
    }
  },

  methods: {
    increment() {
      this.counter.increment()
    }
  }
}

方法2

import { mapStores } from 'vuex'
import { useCounter } from '@/stores/counter'

export default {
  computed: {
    ...mapStores([
      useCounter
    ]),

    count() {
      return this.counter.count  // storeのkey "counter" でアクセスする
    },

    double() {
      return this.counter.double
    }
  },

  methods: {
    increment() {
      this.counter.increment()
    }
  }
}

今までのVuexに似ていますね。computedでStoreの値を利用するか、mapHelperを利用するか、の2択です。後者の方法ではvuex.storeが暗黙的に実行されているようです。ちなみにmapStoresに渡されたStoreにアクセスする際は、difneStoreで定義したkeyの値が利用することになります。

次にCompositon APIで実装されたComponentからのアクセス方法です。

import { useCounter } from '@/stores/Counter'

export default {
  setup() {
    const counter = useCounter()

    counter.count
  }
}

直感的でけっこう好みです。この方法だとStore定義をuseXxxxと命名する意味が実感できますね。

以上が新しいVuexの使い方です。
RFCにはこれ以外にも、Store同士の依存関係、ハイドレーション、型注釈等の解説もありました。

感想

今までは、Vuexのルールや制約の中でどれだけ依存関係を整えられるか、責務を分離できるか、再利用したい部品を適切に再利用できるか、といったことを悩み続けてきました。その軌跡はこのブログ上にもいくつかの記事として残っています。

しかしこれからはComposition API と Composition Storeを使うことで、Store層は極限まで薄く最低限のものになり、Componentに持たせるロジックは切り出され、結果的に状態・View・ロジックがシンプルに分離された構造となりそうです。そしてそれらの依存関係はFluxのように循環するのではなく、Viewが状態やロジックに依存する形になりそうです。

これだけ聞くと、かつてのMVCのFat Contoller問題のような、Componentが依存で膨れ上がる可能性があるのでは……と思いましたがどうなんでしょう。Componentの切り方次第で良くも悪くもなりそうなので、今までとは違った切り口で設計する心の準備をしておこうと思います。

話は変わりますが、React向けの状態管理ライブラリのRecoilにLoadableという非同期処理の状態を扱う仕組みがあるのですが、これに近いことがVuexでもできると良いなと漠然と思っています。stateの初期値を非同期で取得する場面や、ユーザの入力をActionsを通じてServerへ反映する場面などの、非同期の状態の扱いやViewへの反映がお手軽になると嬉しいです。

参考