🦔

Vuex 5でどのように変わるのか?

2021/09/29に公開

はじめに

現在リリースされている Vuex の最新のバージョンは Vuex 4 です。
これは Vuex 3 と互換性のあるバージョンで Vue 3 で使用するためのバージョンであり、 Vuex 3 と同じ API となっています。

そのために現状 Vuex の問題点としてよくあげられている TypeScript サポートの問題点を解決できていません。

Vuex 5 は Vue 3 において Composition API による Reactivity API が登場したことにより Reactivitty API によってどのように Vuex をどうさせるか再考されたバージョンとなっています。

Vuex 5 は以下の点にフォーカスしています。

  • グローバルな状態を定義する
  • コードの分割
  • SSR サポート
  • Vue Devtools のサポート
  • 拡張性

「グローバルな状態を定義する」「コードの分割」という点に関しては Vuex を利用せずとも Composition API を利用することである程度は達成することができるかとは思います。特に Vuex が目指してのはその他の項目のサポートであり、また基本的な状態管理の機能公式のプラグインとして提供することで我々開発者がコアな機能の開発に集中できることです。

Vuex 5 の新機能

Vuex 5 の新機能として以下が上げられます。

  • Options API と Composition API の両方をサポートする
  • 完璧な TypeScript サポート
  • ミューテーションの廃止
  • ネストされたモジュールの廃止
  • 自動的なコードの分割

ミューテーションの廃止

一番インパクトの大きい変更点はミューテーションの廃止でしょうか?

確かに今まで Vuex のコードを書いてきた中においてもミューテーションは単なるセッターとしてのボイラープレートであることが多かったので、妥当な変更なのかもしれません。この変更により、ステートの変更はアクションによりのみ変更されるようになります。

Vuex は元々 Flux などから影響を受けて誕生したましたが、 Flux のアーキテクチャからは脱却するようです。

ネストされたモジュールの廃止

Vuex では namespace を切ってモジュールごとに管理することができました。

しかし、ネストされたモジュールは TypeScript によって型定義をするのが困難であるためVuex 5 ではネストされたモジュールは廃止されます。

ネストされたモジュールに相当する機能を利用したい場合には後述する Store Composition と呼ばれるパターンを使用します。

自動的なコードの分割

後のチュートリアルでもでてきますが Vuex 5 では Vuex インスタンスを作成する際にモジュールを一括で登録するのではなく、コンポーネントごとにストアを登録するようになります。

これにより使用されていないストアをバンドルファイルから取り除くことができるなど webpack 等の Tree Shaking においてメリットが得られます。

Vuex 5 のチュートリアル

Vuex 5 の基本的な使用方法を見ていきましょう。 Vuex 5 におけるストアの定義方法と使用方法は Options API による方法と Composition API による方法の2つに大別されます。

Options API

ストアの定義

まずは Options API によるストアの定義方法です。 defineStore() という関数を利用してストアを定義します。

import { defineStore } from 'vuex'

const useTodo = defineStore()

defineStore() の返り値がストアとなるのですが、 Composition API に合わせて useXXX と命名することが推奨されています。

key プロパティの定義

まず初めに key と呼ばれるプロパティを定義する必要があります。これはストアを特定するために使用されるため、ストア全体で一意の名前にする必要があります。これは Dev Tool や SSR などでストアと特定するために使用されます。また、シリアライズできるよう必ず strigng で定義する必要があります。

import { defineStore } from 'vuex'

const useTodo = defineStore({
  key: 'todo'
})

export default useTodo
state の定義

次に state を定義しましょう。これは従来の Vuex のステートと同じく現在の状態を保持します。変更点としては コンポーネントの data プロパティのようにオブジェクトを返す関数として定義する必要があります。

import { defineStore } from 'vuex'

const useTodo = defineStore({
  key: 'todo',

  state() {
    return {
      todos: []
    }
  }
})

export default useTodo
getters の定義

こちらも従来の Vuex のゲッターと同様です。変更点として従来のゲッターはメソッドの引数として state を受け取りその値を返していたのに対して Vuex 5 では this を通して state にアクセスするようになっています。

import { defineStore } from 'vuex'

const useTodo = defineStore({
  key: 'todo',

  state() {
    return {
      todos: []
    }
  },

  getters: {
    completedTodos() {
      return this.todos.filter(todo => todo.comleted)
    }
  }
})

export default useTodo
actions の定義

最後にアクションです。ミューテーションが廃止されたのでアクションから直接 state を変更します。ゲッターと同様に this を通じて state へアクセスします。

import { defineStore } from 'vuex'

const useTodo = defineStore({
  key: 'todo',

  state() {
    return {
      todos: []
    }
  },

  getters: {
    completedTodos() {
      return this.todos.filter(todo => todo.comleted)
    }
  },

  actions: {
    async fetchTodos() {
      const result = await fetch(`/api/todos`)
      this.todos = await result.json()
    }
  }
})

export default useTodo

ストアの使用

ストアを定義しただけではまだ使用することができないので、定義したストアを使用できるようにする必要があります。

ストアを利用するためにはまず createVuex() 関数により Vuex のインスタンスを生成します。その後 Vuex のインスタンスに先程定義したストアを登録します。

import { createVuex } from 'vuex'
import useTodo from './todo'

const vuex = createVuex()

cosnt todo = vuex.store(useTodo)

ストアを登録した後は以下のように使用することができます。

todo.todos // state へのアクセス

todo.completedTodos // getters へのアクセス

await todo.fetchTodos() // action の呼び出し

従来のゲッターやアクションの呼び出しと比べて純粋な JavaScript のオブジェクトとして呼び出すことができます。そのため、 TypeScript による型に守られてた呼び出しが可能です。

コンポーネント内でストアを使用する

続いてコンポーネント内でストアを使用する方法です。従来の方法と同じように app.use() で Vuex インスタンスを登録します。

import { createApp } from 'vue'
import App from '.App.vue'
import { createStore } from 'vuex'

const app = createApp(App)

app.use(store)

app.mount('#app')

Vue アプリケーションに Vuex を登録する時点では定義したストアを登録をしません。ストアの登録はコンポーネント内で行います。

import { defineComponent } from 'vue'
import useTodo from './todo'

export default defineComponent({
  computed: {
    todo() {
      return this.$vuex.store(useTodo)
    }
  }
})

また、 mapStores ヘルパー関数を使用すれば簡単にストアを登録することもできます。

import { defineComponent } from 'vue'
improt { mapStores } from 'vuex'
import useTodo from './todo'

export default defineComponent({
  computed: {
    ...mapStores({
      todo: useTodo
    })
  }
})

Composition API

続いて Composition API によるストアの定義方法です。こちらは従来のストアの定義方法とは大きく異なり、 composition API を利用したストアの定義となっています。これは コンポーネントの setup 内のロジックをそのまま抽出したようなイメージですね。

import { ref, computed } from 'vue'
import { defindStore } from 'vuex'

const useTodo = defineStore('todo', () => {
  const todos = ref([])

  const completedTodos = computed(() => todo.value.filter(todo => todo.completed))

  const fetchTodo = async () => {
    const result = await fetch(`api/todos`)
    todos.value = await result.json()
  }

  return {
    todos,
    computedTodos,
    fetchTodo
  }
})

export default useTodo

defineStore() 関数は第一引数に key を、第二引数にコールバック関数を受け取ります。コールバック関数の内部は Composition API の setup 関数とまったく同じです。

コンポーネント内で使用

コンポーネント内での使用は簡単で Vue アプリケーションに登録する必要はありません。 setup 関数内で useTodo を呼び出すだけで使用できます。

import { defineComponent } from 'vue'
import useTodo from './todo'

export default defineComponent({
  setup() {
    const todo = useTodo()

    return {
      todo
    }
  }
})

これは Cpmposition API によって定義された Composable 関数使用するときと全く同じように使用できます。

Store Composition

Vuex 5 ではモジュールと呼ばれる概念がありません。Vuex のストア内で他のストアを利用する手段として Store Composition パターンが紹介されています。これは従来のネストされたモジュールであったり rootStaterootGetters の代わりに使用されるものです。

Options API

まずは Options API による方法です。 use プロパティによって他のストアを定義することでそのストアを利用することができます。

下記の例では todo ストア内で auth ストアを利用しています。

// auth.ts
import { defindStore } from 'vuex'

const useAuth = defineStore({
  key: 'auth',

  state() {
    return { 
      user: null
    }
  },

  action() {
    async fetchUser() {
      const result = fetch(`/api/me`)
      this.state = await result.json()
    }
  }
})

export default useAuth
// todo.ts
import { defineStore } from 'vuex'
import useAuth from './auth'

const useTodo = defineStore({
  key: 'todo',

  use() {
    return {
      auth: useAuth
    }
  },

  state() {
    return {
      todos: []
    }
  },

  getters: {
    completedTodos() {
      return this.todos.filter(todo => todo.comleted)
    },
    params() {
      if (!this.auth.user) return ''
      const params = new URLSearchParams({ user_id: this.auth.user.id })
      return `?${params}`
    }
  },

  actions: {
    async fetchTodos() {
      const result = await fetch(`/api/todos${this.params}`)
      this.todos = await result.json()
    }
  }
})

export default useTodo

use プロパティで定義したストアは this からアクセスすることができます。後はコンポーネント等でストアにアクセスするのと変わりありません。

Composition API

Composition API による他のストアの使用はさらにシンプルになっています。他の場所でストアを使用するときとまったく変わりなく使用することができます。

// todo.ts
import { ref, computed } from 'vue'
import { defindStore } from 'vuex'
import useAuth from './auth'

const useTodo = defineStore('todo', () => {
  const auth = useAuth()

  const todos = ref([])

  const completedTodos = computed(() => todo.value.filter(todo => todo.completed))

  const params = computed(() => {
    if (!auth.user) return ''
    const params = new URLSearchParams({ user_id: auth.user.id })
    return `?${params}`
  })

  const fetchTodo = async () => {
    const result = await fetch(`api/todos${params.value}`)
    todos.value = await result.json()
  }

  return {
    todos,
    computedTodos,
    fetchTodo
  }
})

export default useTodo
Component外でストア使用する場合

上記で定義したストアには1点注意点があります。 defineStore で定義したストアを useAuth() の形で使用していますが、内部的にはこれは provide/inject によって定義されています。

provide/inject はコンポーネント内でしか使用できないため、上記で定義したストアはコンポーネント外で使用することができなくなってしまいます。

  const auth = useAuth() // Can't use `inject` here bacause the `useAuth` is called outside of Vue

この問題を解決するために defineStore のコールバック関数の引数から use 関数を受け取り代わりにこれを利用するように修正します。

 // todo.ts
 import { ref, computed } from 'vue'
 import { defindStore } from 'vuex'
 import useAuth from './auth'

- const useTodo = defineStore('todo', () => {
-  const auth = useAuth()
+ const useTodo = defineStore('todo', ({ use }) => {
+  const auth = use(useAuth)

   const todos = ref([])

   const completedTodos = computed(() => todo.value.filter(todo => todo.completed))

   const params = computed(() => {
     if (!auth.user) return ''
     const params = new URLSearchParams({ user_id: auth.user.id })
     return `?${params}`
   })

   const fetchTodo = () => {
     const result = await fetch(`api/todos${params.value}`)
     todos.value = await result.json()
   }

   return {
     todos,
     computedTodos,
     fetchTodo
   }
 })

 export default useTodo

しかし、この構文は少し奇妙でありコンポーネントの setup 関数との互換性がなくなってしまいます。

そのため Vuex インスタンスをシングルトンそしてグローバルに登録する構文が提案されているようです。

プラグイン

拡張性の観点として Vuex 5はプラグインサポートの機能を持っています。
例えば、ストア内で axios を使用したい場合には次のような例が紹介されています。

import { createVuex } from 'vuex'
import axios from 'axios'

function axiosPlugin(vuex, options) {
  vuex.storeProperties.$axios = axios
}

const vuex = createVuex({
  plugins: [axiosPlugin]
})

Options API では this から注入したプラグインにアクセスできます。

import { defineStore } from 'vuex'

const useTodo = defineStore({
  key: 'todo',

  actions: {
    async fetchTodos() {
      const { data } = await this.$axios.get(`/api/todos`)
      this.todos = data
    }
  }
})

export default useTodo

Composition API では、defineStore のコールバック関数の引数としてプラグインを注入します。

import { defindStore } from 'vuex'

const useTodo = defineStore('todo', ({ $axios }) => {
  const fetchTodo = async () => {
    const { data } = await $axios.get(`api/todos`)
    todos.value = data
  }

  return {
    fetchTodo
  }
})

export default useTodo

TypeScript でプラグインを使用する場合には、下記のように StoreCustomeProperties に適切な型を与える必要があります。

import { AxiosInstance } from 'axios'

declare module 'vuex' {
  interface StoreCustomProperties {
    $axios: AxiosInstance
  }
}

まとめ

  • Vuex は flux から脱却する
  • Options APIと Composition APIの2通りの書き方があり、どちらもコンポーネントに近い書き方となる
  • TypeScript を完全にサポート

感想

Vuex 5 から従来の Vuex から大きく変えてきましたね。
状態管理のために特別な書き方を要求されるのではなく、コンポーネント内のロジックをそのまま切り出しただけのようなシンプルなものになりました。

Composition API の論理的な関心事に分割するという思想にもマッチしているので Composition API を覚えてしまえばそのまま使用できるのは良い点だと思います。

ただし、従来のVuex からのマイグレーションはかなり大変そうですね。

Vuex の TypeScript サポートもようやく来たって感じですね。若干他のフレームワークに比べて出遅れてる感は否めないですが今後の動向にも期待しましょう。

参考

https://github.com/kiaking/rfcs/blob/vuex-5/active-rfcs/0000-vuex-5.md

https://www.youtube.com/watch?v=WmgQH4pOhUc

GitHubで編集を提案

Discussion