はじめに
Vue.jsでアプリが大きくなってくると、複数のコンポーネントで同じデータを共有したくなる場面が増えてきます。propsのバケツリレーやイベントの発行元をあちこちに散らばせているうちに、「今このstateを書き換えているのはどのコンポーネントなのか」が追えなくなった経験がある方も多いのではないでしょうか。
Vuexは、そんなVue.jsアプリケーションの状態を1箇所に集約するための、公式の状態管理ライブラリです。Reactの世界でいうReduxと同じくFluxパターンに基づいており、「状態はどこから来て、どう変わったのか」を常に追跡できる設計になっています。
なお正直にお伝えすると、Vue公式チームは新規プロジェクトでは後継のPiniaを推奨しており、Vuexは現在メンテナンスモードに入っています。とはいえ既存プロジェクトでの採用実績は非常に多く、FluxパターンやMutations/Actionsという設計そのものを理解しておく価値は今でも十分にあります。
とはいえ、読むより触った方が早いと思います。state・mutations・actions・gettersの4要素が実際にどう動くのか、先に挙動を見たい方はこちらからどうぞ。
Vuexとは
Vuexは、Vue.jsアプリケーション向けの集中管理型状態管理ライブラリです。「Centralized State Management for Vue.js」を謳っており、Flux/Reduxと同様に単方向のデータフローでアプリケーション全体の状態を管理します。現行のVuex 4はVue 3に対応しており、Composition APIとも組み合わせて使えます。
主な特徴
- 単一の状態木(Single State Tree) - アプリ全体のstateを1つのstoreオブジェクトに集約し、どこからでも同じ状態を参照できる
- Mutationsによる予測可能な変更 - stateの変更は必ずMutationsを経由するため、いつ・何が状態を変えたのかを追跡しやすい
- Actionsによる非同期処理の分離 - 非同期ロジックはActionsに切り出し、Mutationsは同期的な変更だけに専念させる
- namespaced modulesによる分割 - 機能単位でstoreを分割し、大規模アプリでも見通しよく管理できる
- Vue DevTools対応 - タイムトラベルデバッグやMutationsの履歴確認が可能
インストール
npm、yarn、pnpmのいずれでも導入できます。Vue 3対応版は以下のコマンドでインストールします。
npm install vuex@next --save
yarn add vuex@next --save
pnpm add vuex@next
Vue 2プロジェクトの場合は vuex@3 を指定してください。
Vuexのサンプルを動かす
以下はcreateStoreでstoreを作り、state・mutations・actions・gettersをひととおり使ったカウンターです。「同期でincrement」ボタンはstore.commitを直接呼び出し、「非同期でincrement」ボタンはstore.dispatch経由でActionsを呼び出しています。stepの値を変えると、増加量が変わるのを確認できます。
const store = createStore({
state() {
return { count: 0 }
},
mutations: {
increment(state, payload) {
state.count += payload
},
},
actions: {
incrementAsync({ commit }, payload) {
setTimeout(() => commit('increment', payload), 500)
},
},
getters: {
doubleCount: (state) => state.count * 2,
},
})
store.commit('increment', 3) // 同期的にstateを変更
store.dispatch('incrementAsync', 2) // 非同期処理を経てmutationを呼ぶ
実際に動かせるものが下です。stepの数値を変えてから2つのボタンを押し比べてみてください。
store.commitはMutationsを即座に実行するのに対し、store.dispatchはActions経由で非同期処理を挟んでからMutationsを呼び出します。この「同期はMutations、非同期はActions」という役割分担がVuexの中心的な設計思想です。
基本的な使い方
実プロジェクトでは、storeの定義・アプリへの登録・コンポーネントからの利用を別ファイルに分けるのが一般的です。
<!-- store/index.js -->
<script>
import { createStore } from 'vuex'
export default createStore({
state() {
return { count: 0 }
},
mutations: {
increment(state) {
state.count++
},
},
})
</script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')
<!-- Counter.vue -->
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const count = computed(() => store.state.count)
</script>
<template>
<button @click="store.commit('increment')">count: {{ count }}</button>
</template>
useStore()はComposition API向けのヘルパーで、app.use(store)で登録されたstoreをコンポーネント内から取得できます。
実践的なユースケース
namespaced modulesで機能ごとにstoreを分割する
アプリが大きくなるとstoreも肥大化します。Vuexはmodulesオプションで機能単位にstoreを分割でき、さらにnamespaced: trueを付けることでMutations・Actions・Gettersの名前が衝突しなくなります。
const cartModule = {
namespaced: true,
state: () => ({ items: [] }),
mutations: {
add(state, item) {
state.items.push(item)
},
},
getters: {
total: (state) => state.items.reduce((sum, i) => sum + i.price, 0),
},
}
const store = createStore({
modules: { cart: cartModule },
})
store.commit('cart/add', { name: 'ペン', price: 120 })
store.getters['cart/total'] // => 120
商品名と価格を入力してカートに追加すると、cartモジュールのtotalゲッターが自動で合計を計算します。
Mutation名・Getter名を'cart/add'や'cart/total'のようにモジュール名/名前で指定するのがnamespaced modulesの流儀です。モジュールを増やしても、他のモジュールの同名Mutationと衝突しません。
非同期Actionsでデータ取得とエラーを扱う
API通信のような非同期処理はActionsに書きます。ローディング状態やエラーもstateとして持たせておくと、UI側は状態を見るだけで表示を切り替えられます。
actions: {
async fetchUsers({ commit }, shouldFail) {
commit('setLoading', true)
try {
const users = await fakeApi(shouldFail) // 疑似的なAPI呼び出し
commit('setUsers', users)
commit('setError', null)
} catch (e) {
commit('setError', e.message)
} finally {
commit('setLoading', false)
}
},
}
「エラーを発生させる」にチェックを入れてから取得すると失敗パス、外した状態なら成功パスを確認できます。
Actionsの引数にある{ commit }はcontextオブジェクトの分割代入で、ここから他のMutationsを呼んだり、context.stateで現在の状態を参照したりできます。
store.subscribeで状態変更をlocalStorageに永続化する
Vuexのstore.subscribeを使うと、Mutationsが実行されるたびにコールバックを実行できます。これを使えばstateの変更をlocalStorageに書き出す永続化処理を、コンポーネントの外側に一箇所だけ書けます。
const store = createStore({
state: () => ({ memo: localStorage.getItem('memo') || '' }),
mutations: {
setMemo(state, text) {
state.memo = text
},
},
})
store.subscribe((mutation, state) => {
if (mutation.type === 'setMemo') {
localStorage.setItem('memo', state.memo)
}
})
テキストエリアに入力するたびにsetMemoミューテーションが呼ばれ、store.subscribeのコールバックがlocalStorageへ書き込みます。リセットボタンで保存内容をクリアできます。
store.subscribeはログ送信や分析イベントの発火など、Mutationsをフックしたい場面全般で使えます。Vue DevTools拡張のタイムトラベルデバッグも、内部的にはこの仕組みを利用しています。
まとめ
Vuexは、単一の状態木・Mutationsによる同期的な変更・Actionsによる非同期処理の分離という、Fluxパターンに基づいた明快な設計を持つ状態管理ライブラリです。namespaced modulesで大規模化にも対応でき、store.subscribeのような低レベルAPIも用意されています。
Vue公式チームは新規プロジェクトではPiniaを推奨しており、Vuexは現在メンテナンスモードです。それでも、Mutations/Actionsという型を通じて「状態変更をどう追跡可能にするか」を学べる点は、PiniaやReduxを含む他の状態管理ライブラリを理解するうえでも役立ちます。既存のVuexプロジェクトに関わる方はもちろん、状態管理の設計そのものに興味がある方も、ぜひ手を動かして試してみてください。
