Vue.jsの状態管理 Vuex 入門ガイド:詳細解説
はじめに:なぜ状態管理が必要なのか?
モダンなフロントエンドアプリケーション開発において、状態管理は非常に重要な課題です。ここで言う「状態」とは、アプリケーション全体で共有されるデータのことです。例えば、ログインしているユーザーの情報、ショッピングカートの中身、画面の表示状態(モーダルが開いているか、ローディング中かなど)などがこれにあたります。
Vue.jsのようなコンポーネントベースのフレームワークを使用していると、アプリケーションは多くの独立したコンポーネントで構成されます。これらのコンポーネント間でデータをやり取りする方法はいくつかあります。
- Propsによる親から子へのデータの受け渡し: これは最も基本的で推奨される方法ですが、ネストが深くなるにつれて、データを受け渡すためだけに中間コンポーネントを経由する必要が出てくる「Props Drilling」という問題が発生しやすくなります。
- イベントによる子から親へのデータの通知:
$emit
を使って子コンポーネントからイベントを発行し、親コンポーネントでそれを受け取ることで、子から親へのデータや状態の変更を伝えることができます。 - Provide/Inject: 親コンポーネントが提供(Provide)したデータを、任意の子孫コンポーネントが注入(Inject)して使用できます。これはProps Drillingを回避するのに役立ちますが、データのリアクティビティや変更の追跡が難しくなる場合があります。
- イベントバス: 空のVueインスタンスをイベントバスとして利用し、コンポーネント間でイベントを発行・購読することで、親子の関係にかかわらずコンポーネント間で通信できます。これはシンプルですが、アプリケーションが大規模になると、どのコンポーネントがどのイベントを発行・購読しているかの管理が困難になり、デバッグが難しくなる可能性があります。
これらの方法でも小規模なアプリケーションであれば問題なく開発できます。しかし、アプリケーションが成長し、共有される状態が増え、その状態を変更する操作が複数の場所から行われるようになると、データフローが複雑になり、以下の問題が発生しやすくなります。
- 状態の追跡が困難: どのコンポーネントが状態をどのように変更したのかが分かりにくくなる。
- デバッグの困難さ: 状態がおかしい場合に、原因となっている変更箇所を特定するのが難しい。
- コンポーネント間の密結合: 状態を共有するためにコンポーネント同士が強く依存し合うようになる。
- コードの再利用性の低下: 特定の状態に依存したコンポーネントは、他の場所で再利用しにくくなる。
このような問題を解決するために登場するのが、状態管理パターンです。状態管理パターンでは、アプリケーション全体の共有される状態を一箇所(ストア)に集約します。そして、そのストアの状態を変更する操作を特定のルールに従って行うことで、データフローを予測可能にし、状態の変更を追跡しやすくします。
Vue.jsの公式な状態管理ライブラリがVuexです。Vuexは、FluxやReduxといった他の状態管理ライブラリの概念を取り入れつつ、Vue.jsのエコシステムに最適化されています。特に、Vue.jsのリアクティビティシステムと深く連携しているため、非常に効率的に状態管理を行うことができます。
この記事で学ぶこと
この記事では、Vuexの基本的な概念から、実際にアプリケーションに導入する方法、そして少し応用的なトピックまでを詳しく解説します。具体的には、以下の内容を扱います。
- Vuexの主要な構成要素(State, Getters, Mutations, Actions, Modules)の詳細な説明
- Vuexストアの作成とVueアプリケーションへの組み込み方
- Vuexを使った簡単なカウンターアプリケーションの実装例
- Vuexを使ったより実践的なTodoアプリケーションの実装例
mapState
,mapGetters
,mapMutations
,mapActions
といったヘルパー関数の使い方- Vuexの応用的なトピック(Strict Mode, プラグイン, テストの概要)
- Vuexを使う上でのベストプラクティスと注意点
この記事を読むことで、あなたはVuexを使ってVue.jsアプリケーションの複雑な状態を効率的に管理できるようになるでしょう。
Vuexの基本概念 (Core Concepts of Vuex)
Vuexは、アプリケーションのセントラライズド・ストアパターンを実装するライブラリです。このストアには、アプリケーションの全てのコンポーネントからアクセスできる共有された状態が保持されます。Vuexストアの中心的な要素は以下の5つです。
- State: アプリケーションの全ての共有状態(データ)を保持する場所。
- Getters: Stateから派生した状態を計算するためのもの。Vueの算出プロパティに似ています。
- Mutations: Stateを変更するための唯一の方法。必ず同期的な処理である必要があります。
- Actions: Mutationsをコミットするためのもの。非同期処理を含むことができます。
- Modules: 大規模なアプリケーションでストアを分割するための仕組み。
これらの要素が連携して、予測可能な状態管理を実現します。そのデータフローは以下のようになります。
Vueコンポーネント -> Actionをディスパッチ -> ActionがMutationをコミット -> MutationがStateを変更 -> Stateの変更がコンポーネントに反映
この一方向のデータフローが、状態の変更を追跡しやすくし、アプリケーション全体のデバッグ性を向上させます。
それでは、各要素を詳しく見ていきましょう。
1. State: 唯一の真実のソース (Single Source of Truth)
Stateは、Vuexストアの中心であり、アプリケーション全体で共有される全てのデータがここに保持されます。例えるなら、アプリケーションの「データベース」のようなものです。Vuexの重要な原則の一つは、Stateを直接変更してはならないということです。Stateの変更は、後述するMutationsを介してのみ行われます。
Stateの定義:
Stateは、Vuexストアを定義する際の state
オプションとして、シンプルなオブジェクトとして定義します。
“`javascript
// store.js
import { createStore } from ‘vuex’; // Vuex 4の場合
const store = createStore({
state: {
count: 0,
user: null,
isLoading: false,
todoList: []
},
// …他のオプション (getters, mutations, actions, modules)
});
export default store;
“`
Stateへのアクセス:
VueコンポーネントからStateにアクセスする方法はいくつかあります。
-
$store.state
を使う: コンポーネントのインスタンス内では、this.$store
を通じてストアにアクセスできます。Stateはthis.$store.state
から取得できます。“`vue
現在のカウント: {{ $store.state.count }}
ユーザー: {{ $store.state.user.name }}
“`これはシンプルですが、テンプレートや計算プロパティ内で State の複数のプロパティを参照する場合、コードが冗長になりがちです。
-
mapState
ヘルパー関数を使う: Vuexが提供するmapState
ヘルパー関数を使うと、ストアのStateプロパティをコンポーネントの計算プロパティにマッピングできます。これにより、テンプレートやスクリプト内でthis.someStateProp
のように State にアクセスできるようになり、コードがよりクリーンになります。“`vue
現在のカウント: {{ count }}
ユーザー: {{ user.name }}
“`mapState
は、計算プロパティとしてStateをマッピングするため、Stateが変更されると自動的にコンポーネントがリアクティブに更新されます。これは非常に便利で、推奨される方法です。スプレッド演算子 (...
) を使って、他のローカル計算プロパティと組み合わせるのが一般的なパターンです。
2. Getters: Stateから派生した状態
Gettersは、ストアのStateを元に、計算された値を提供します。Vueの算出プロパティ(Computed Properties)のストア版のようなものです。例えば、ユーザーリストのStateがあるときに、アクティブなユーザーだけをフィルタリングして取得したい場合などにGettersを使います。Gettersは、Stateが変更されると自動的に再計算され、結果はキャッシュされます(算出プロパティと同様)。
Gettersの定義:
Gettersは、Vuexストアを定義する際の getters
オプションとして定義します。各Getter関数は、第一引数に state
を受け取ります。
“`javascript
// store.js
import { createStore } from ‘vuex’;
const store = createStore({
state: {
todos: [
{ id: 1, text: ‘Vuex入門’, done: true },
{ id: 2, text: ‘Stateを理解する’, done: false },
{ id: 3, text: ‘Mutationを使う’, done: false }
]
},
getters: {
// Getter関数は第一引数にstateを受け取る
doneTodos (state) {
return state.todos.filter(todo => todo.done);
},
// 別のGetterを第二引数に受け取ることも可能
doneTodosCount (state, getters) {
return getters.doneTodos.length;
},
// Getterは関数を返すことで引数を受け取ることができる
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id);
}
}
});
“`
Gettersへのアクセス:
VueコンポーネントからGettersにアクセスする方法もいくつかあります。
-
$store.getters
を使う: コンポーネント内でthis.$store.getters
からアクセスできます。vue
<template>
<div>
<p>完了したTodoの数: {{ $store.getters.doneTodosCount }}</p>
<p>ID=2のTodo: {{ $store.getters.getTodoById(2)?.text }}</p>
</div>
</template> -
mapGetters
ヘルパー関数を使う:mapGetters
ヘルパー関数を使うと、ストアのGettersをコンポーネントの計算プロパティにマッピングできます。mapState
と同様に、コードをクリーンに保てます。“`vue
完了したTodoの数: {{ doneTodosCount }}
ID=2のTodo: {{ $store.getters.getTodoById(2)?.text }}
“`mapGetters
もmapState
と同様に、計算プロパティとしてGetterをマッピングするため、関連するStateが変更されると自動的に更新されます。引数を受け取るGetter(例:getTodoById
)は、mapGetters
では計算プロパティとして直接マッピングできません。その場合は$store.getters
を使用する必要があります。
3. Mutations: Stateを変更する唯一の方法
Mutationは、VuexストアのStateを変更するための唯一の公式な方法です。Mutationは常に同期的な処理である必要があります。なぜ同期的な必要があるのかというと、Vuex Devtoolsを使って状態の変化を正確に追跡(タイムトラベルデバッグ)するためです。もしMutationが非同期だと、Stateがいつ変更されたかを追跡するのが非常に難しくなります。
Stateを直接変更するのではなく、Mutationを介して変更することで、全ての状態変更が明確な意図を持って行われ、変更履歴を追跡できるようになります。
Mutationの定義:
Mutationは、Vuexストアを定義する際の mutations
オプションとして定義します。各Mutation関数は、第一引数に state
を受け取ります。第二引数には、 Mutation に渡される任意のペイロード (Payload) を受け取ることができます。
“`javascript
// store.js
import { createStore } from ‘vuex’;
const store = createStore({
state: {
count: 0,
user: null
},
mutations: {
// Mutation名は慣習的に大文字で書かれることが多い
INCREMENT (state) {
// Stateを直接変更
state.count++;
},
DECREMENT (state) {
state.count–;
},
// ペイロードを受け取るMutation
INCREMENT_BY (state, amount) {
state.count += amount;
},
SET_USER (state, user) {
state.user = user;
}
},
// …他のオプション
});
“`
Mutationの実行 (Committing Mutations):
Mutationを実行することをコミットする (commit) と呼びます。コンポーネントやActionsからMutationをコミットするには、$store.commit
メソッドを使用します。
javascript
// コンポーネントのメソッドやライフサイクルフックから
methods: {
increment() {
// Mutation名だけを指定
this.$store.commit('INCREMENT');
},
incrementBy(amount) {
// Mutation名とペイロードを指定
this.$store.commit('INCREMENT_BY', amount);
},
setUser(user) {
this.$store.commit('SET_USER', user);
}
}
ペイロードとして複数の値を渡したい場合は、オブジェクトとして渡すのが一般的です。
javascript
this.$store.commit('SET_USER', { name: 'Alice', id: 1 });
mapMutations
ヘルパー関数を使う:
mapMutations
ヘルパー関数を使うと、ストアのMutationsをコンポーネントのメソッドにマッピングできます。これにより、this.$store.commit('MUTATION_NAME', payload)
と書く代わりに、this.mutationName(payload)
のように呼び出せるようになります。
“`vue
“`
mapMutations
を使うことで、テンプレートやスクリプト内のMutationコミットの記述が簡潔になります。
4. Actions: 非同期処理とMutationのコミット
Actionsは、Mutationsをコミットするために使用されます。Mutationは同期である必要がありますが、Actionsは非同期処理を含むことができます。例えば、APIからデータを取得したり、複数のMutationを順番にコミットしたりといった処理はActionsで行います。
ActionsはStateを直接変更しません。必ずMutationをコミットすることでStateを変更します。
Actionsの定義:
Actionsは、Vuexストアを定義する際の actions
オプションとして定義します。各Action関数は、第一引数に context
オブジェクトを受け取ります。context
オブジェクトはストアインスタンスの多くのメソッドやプロパティ(state
, getters
, commit
, dispatch
など)を公開します。第二引数には、Action に渡される任意のペイロードを受け取ることができます。
“`javascript
// store.js
import { createStore } from ‘vuex’;
import axios from ‘axios’; // 例としてaxiosを使用
const store = createStore({
state: {
count: 0,
userData: null
},
mutations: {
SET_COUNT (state, count) {
state.count = count;
},
SET_USER_DATA (state, userData) {
state.userData = userData;
}
},
actions: {
// contextオブジェクトを受け取る
incrementAsync (context) {
return new Promise((resolve) => {
setTimeout(() => {
// MutationをコミットしてStateを変更
context.commit(‘SET_COUNT’, context.state.count + 1);
resolve();
}, 1000);
});
},
// ペイロードを受け取るAction
incrementByAsync (context, amount) {
return new Promise((resolve) => {
setTimeout(() => {
context.commit(‘SET_COUNT’, context.state.count + amount);
resolve();
}, 1000);
});
},
// contextを分割代入することも可能
async fetchUserData ({ commit }, userId) {
try {
// 非同期APIコール
const response = await axios.get(/api/users/${userId}
);
const userData = response.data;
// 取得したデータでMutationをコミット
commit(‘SET_USER_DATA’, userData);
} catch (error) {
console.error(‘Failed to fetch user data:’, error);
// エラーハンドリング(例えば、エラー状態をストアに保存するMutationをコミットするなど)
// commit(‘SET_ERROR’, error);
throw error; // エラーを再スローして呼び出し元に伝える
}
},
// 複数のMutationや他のActionを呼び出すことも可能
complexAction ({ commit, dispatch }, payload) {
commit(‘START_PROCESSING’); // ローディング状態を開始するMutation
dispatch(‘anotherAction’, payload.part1) // 別のActionを呼び出す
.then(() => {
commit(‘PROCESS_PART2’, payload.part2); // Mutationをコミット
commit(‘END_PROCESSING’); // ローディング状態を終了するMutation
})
.catch(error => {
commit(‘HANDLE_ERROR’, error);
commit(‘END_PROCESSING’);
});
}
},
// …他のオプション
});
“`
Actionの実行 (Dispatching Actions):
Actionを実行することをディスパッチする (dispatch) と呼びます。コンポーネントや他のActionsからActionをディスパッチするには、$store.dispatch
メソッドを使用します。
javascript
// コンポーネントのメソッドから
methods: {
// Action名だけを指定
triggerIncrementAsync() {
this.$store.dispatch('incrementAsync');
},
// Action名とペイロードを指定
triggerIncrementByAsync(amount) {
this.$store.dispatch('incrementByAsync', amount);
},
// 非同期Actionの場合、Promiseを返すのでawaitできる
async loadUserData(userId) {
try {
await this.$store.dispatch('fetchUserData', userId);
console.log('User data loaded successfully!');
} catch (error) {
console.log('Failed to load user data.');
}
}
}
$store.dispatch
は、実行されたActionから返されたPromiseを返すため、非同期処理が完了した後の処理をチェーンしたり、async/await
を使って処理の流れを分かりやすく記述したりすることができます。
mapActions
ヘルパー関数を使う:
mapActions
ヘルパー関数を使うと、ストアのActionsをコンポーネントのメソッドにマッピングできます。
“`vue
“`
mapActions
を使うことで、Actionsのディスパッチの記述も簡潔になります。非同期Actionをマッピングした場合、マッピングされたメソッドもPromiseを返します。
5. Modules: ストアの分割
アプリケーションが大規模になると、単一のストアファイルに全てのState, Getters, Mutations, Actionsを定義するのは管理が難しくなります。Vuexでは、ストアをモジュール (Modules) に分割する機能を提供しています。各モジュールは独自のState, Getters, Mutations, Actions、さらにネストされたModulesを持つことができます。
Moduleの定義:
Moduleは、ルートストアと同様にState, Getters, Mutations, Actionsを持つオブジェクトとして定義します。
“`javascript
// modules/counter.js
const counterModule = {
state: () => ({
count: 0
}),
mutations: {
INCREMENT (state) {
state.count++;
}
},
actions: {
increment ({ commit }) {
commit(‘INCREMENT’);
}
},
getters: {
doubleCount (state) {
return state.count * 2;
}
}
};
export default counterModule;
// modules/user.js
const userModule = {
state: () => ({
user: null,
isLoggedIn: false
}),
mutations: {
SET_USER (state, user) {
state.user = user;
state.isLoggedIn = !!user;
},
CLEAR_USER (state) {
state.user = null;
state.isLoggedIn = false;
}
},
actions: {
async login ({ commit }, credentials) {
// APIコールなど…
const user = { name: credentials.username }; // 仮のユーザーデータ
commit(‘SET_USER’, user);
},
logout ({ commit }) {
commit(‘CLEAR_USER’);
}
},
getters: {
currentUser (state) {
return state.user;
},
isUserLoggedIn (state) {
return state.isLoggedIn;
}
}
};
export default userModule;
“`
ルートストアへのModuleの追加:
定義したModuleは、ルートストアの modules
オプションにオブジェクトとして追加します。オブジェクトのキーが、そのモジュールの名前空間になります(後述)。
“`javascript
// store/index.js
import { createStore } from ‘vuex’;
import counterModule from ‘./modules/counter’;
import userModule from ‘./modules/user’;
const store = createStore({
// ルートState (Moduleに含まれないグローバルな状態)
state: {
appName: ‘My Vuex App’
},
getters: {
// ルートGetter
appName (state) {
return state.appName;
}
},
mutations: {
// ルートMutation
},
actions: {
// ルートAction
},
modules: {
counter: counterModule, // ‘counter’ という名前空間でcounterModuleを登録
user: userModule // ‘user’ という名前空間でuserModuleを登録
}
});
export default store;
“`
名前空間 (Namespacing):
デフォルトでは、Modules内のState, Getters, Mutations, Actionsは、ルートストアにグローバルに登録されます。つまり、異なるモジュールで同じMutation名やAction名を使用すると、衝突が発生する可能性があります。これを避けるために、Moduleに namespaced: true
オプションを追加することで、そのモジュールを名前空間付き (namespaced) にすることができます。
javascript
// modules/counter.js (namespaced: true を追加)
const counterModule = {
namespaced: true, // これを追加
state: () => ({
count: 0
}),
mutations: {
INCREMENT (state) {
state.count++;
}
},
actions: {
increment ({ commit }) {
// 名前空間付きモジュール内では、同じモジュールのMutationをコミットする際は名前空間は不要
commit('INCREMENT');
}
},
getters: {
doubleCount (state) {
return state.count * 2;
}
}
};
export default counterModule;
名前空間付きModule内の要素にアクセスする際は、モジュール名(名前空間)をプレフィックスとして付けます。
- State:
$store.state.moduleName.propName
- 例:
$store.state.counter.count
- 例:
- Getters:
$store.getters['moduleName/getterName']
- 例:
$store.getters['counter/doubleCount']
- 例:
- Mutations:
$store.commit('moduleName/mutationName', payload)
- 例:
$store.commit('counter/INCREMENT')
- 例:
- Actions:
$store.dispatch('moduleName/actionName', payload)
- 例:
$store.dispatch('counter/increment')
- 例:
ヘルパー関数の使い方 (名前空間付きモジュール):
mapState
, mapGetters
, mapMutations
, mapActions
ヘルパー関数を名前空間付きモジュールで使用する場合、第一引数にモジュール名を文字列で指定します。
“`vue
Counter Count: {{ count }}
Counter Double Count: {{ doubleCount }}
“`
名前空間付きモジュールは、大規模なアプリケーションでコードを整理し、名前の衝突を防ぐために非常に有効です。特別な理由がない限り、Moduleには namespaced: true
を設定することを推奨します。
Vuexストアの作成とアプリケーションへの導入
Vuexストアを実際に作成し、Vueアプリケーションに組み込む手順を見ていきましょう。
-
Vuexのインストール:
npmまたはyarnを使ってVuexをインストールします。
“`bash
npm install vuex@next # Vue 3 の場合 (Vuex v4)または
yarn add vuex@next # Vue 3 の場合 (Vuex v4)
Vue 2 の場合 (Vuex v3)
npm install vuex –save
または
yarn add vuex
“`
-
ストアインスタンスの作成:
通常、
src
ディレクトリ配下にstore
というディレクトリを作成し、その中にストアの定義ファイル(例:store/index.js
)を作成します。“`javascript
// src/store/index.js
import { createStore } from ‘vuex’; // Vue 3 の場合// Vue 2 の場合:
// import Vue from ‘vue’;
// import Vuex from ‘vuex’;
// Vue.use(Vuex);const store = createStore({ // Vue 3: createStore
// Vue 2: new Vuex.Store({
state: {
// 状態を定義
count: 0
},
getters: {
// 派生状態を定義
doubleCount(state) {
return state.count * 2;
}
},
mutations: {
// 状態変更のロジックを定義 (同期)
increment(state) {
state.count++;
}
},
actions: {
// 非同期処理や複数のmutationをコミットするロジックを定義
incrementAsync({ commit }) {
setTimeout(() => {
commit(‘increment’);
}, 1000);
}
},
modules: {
// モジュールを登録 (省略可)
}
});export default store;
“` -
ルートVueインスタンスへのストアの連携:
作成したストアインスタンスを、アプリケーションのルートVueインスタンス(通常は
src/main.js
)に渡します。“`javascript
// src/main.js
import { createApp } from ‘vue’; // Vue 3 の場合
import App from ‘./App.vue’;
import store from ‘./store’; // 作成したストアをインポートconst app = createApp(App); // Vue 3
// Vue 2 の場合:
// import Vue from ‘vue’;
// import App from ‘./App.vue’;
// import store from ‘./store’;
// new Vue({
// el: ‘#app’,
// store, // ストアをオプションとして渡す
// render: h => h(App)
// });app.use(store); // Vue 3: ストアをuse()で登録
app.mount(‘#app’);
“`
これで、アプリケーション内のどのコンポーネントからでも this.$store
を通じてVuexストアにアクセスできるようになります。
簡単なカウンターアプリを例にした実装
上記のストア定義を使って、簡単なカウンターアプリケーションを作成してみましょう。
“`vue
Counter App
Count: {{ count }}
Double Count: {{ doubleCount }}
“`
このコンポーネントを App.vue
などで表示すれば、Vuexを使ったカウンターアプリが動作します。
mapState
, mapGetters
, mapMutations
, mapActions
を使うことで、コンポーネント内のコードが簡潔になり、ストアとの連携が分かりやすくなります。
実践例:Todoアプリケーション
より実践的な例として、Todoアプリケーションの状態管理にVuexを導入してみましょう。このアプリケーションでは、Todoの追加、完了/未完了の切り替え、削除、そして完了したTodoの数を表示できるものとします。
1. ストアの設計
まず、ストアに必要な状態、およびそれを操作するためのMutation、Action、Getterを考えます。
- State:
todos
: Todoアイテムの配列。各アイテムは{ id: number, text: string, done: boolean }
のような構造を持つ。
- Getters:
allTodos
: 全てのTodoを返す。doneTodos
: 完了済みのTodoだけを返す。pendingTodos
: 未完了のTodoだけを返す。doneTodosCount
: 完了済みTodoの数を返す。
- Mutations:
ADD_TODO
: 新しいTodoを追加する。TOGGLE_TODO
: 指定されたIDのTodoの完了状態を切り替える。REMOVE_TODO
: 指定されたIDのTodoを削除する。
- Actions:
addTodo
:ADD_TODO
Mutationをコミットする。将来的にはTodo IDの生成などをここで行える。toggleTodo
:TOGGLE_TODO
Mutationをコミットする。removeTodo
:REMOVE_TODO
Mutationをコミットする。- 将来的には、サーバーとのやり取り(Todoの保存、取得など)はActionsで行う。
2. ストアの実装 (src/store/modules/todos.js
)
Todoモジュールを作成し、namespaced: true
とします。
“`javascript
// src/store/modules/todos.js
import { v4 as uuidv4 } from ‘uuid’; // ユニークID生成のためにuuidライブラリを使用
// uuidをインストール: npm install uuid または yarn add uuid
// @types/uuidもインストール: npm install @types/uuid –save-dev または yarn add @types/uuid –dev (TypeScriptの場合)
const todosModule = {
namespaced: true, // 名前空間を有効にする
state: () => ({
todos: [] // Todoアイテムの配列
}),
getters: {
allTodos: (state) => state.todos,
doneTodos: (state) => state.todos.filter(todo => todo.done),
pendingTodos: (state) => state.todos.filter(todo => !todo.done),
doneTodosCount: (state, getters) => getters.doneTodos.length,
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id);
}
},
mutations: {
ADD_TODO (state, text) {
if (!text) return;
state.todos.push({
id: uuidv4(), // ユニークなIDを生成
text,
done: false
});
},
TOGGLE_TODO (state, id) {
const todo = state.todos.find(todo => todo.id === id);
if (todo) {
todo.done = !todo.done;
}
},
REMOVE_TODO (state, id) {
state.todos = state.todos.filter(todo => todo.id !== id);
}
},
actions: {
// MutationをコミットするだけのシンプルなAction
addTodo ({ commit }, text) {
commit(‘ADD_TODO’, text);
},
toggleTodo ({ commit }, id) {
commit(‘TOGGLE_TODO’, id);
},
removeTodo ({ commit }, id) {
commit(‘REMOVE_TODO’, id);
},
// 非同期処理を含む可能性のあるAction例 (ここでは同期的にMutationを呼び出すだけ)
async fetchTodos ({ commit }) {
// サーバーからTodoを取得する処理をシミュレート
// const response = await fetch(‘/api/todos’);
// const todos = await response.json();
const dummyTodos = [
{ id: uuidv4(), text: ‘ストアをセットアップする’, done: true },
{ id: uuidv4(), text: ‘Todoリストコンポーネントを作成する’, done: false },
{ id: uuidv4(), text: ‘Vuexとコンポーネントを連携する’, done: false }
];
// Mutationを追加して、取得したtodosでStateを置き換える(あるいは追加する)
// commit(‘SET_TODOS’, dummyTodos); // 必要に応じてSET_TODOS mutationを作成
dummyTodos.forEach(todo => commit(‘ADD_TODO’, todo.text)); // シンプルに追加する例
}
}
};
export default todosModule;
“`
3. ルートストアへのモジュールの登録 (src/store/index.js
)
作成したtodos
モジュールをルートストアに登録します。
“`javascript
// src/store/index.js
import { createStore } from ‘vuex’;
import todosModule from ‘./modules/todos’; // todosモジュールをインポート
const store = createStore({
state: {
// グローバルな状態があればここに
},
getters: {
// グローバルなGetterがあればここに
},
mutations: {
// グローバルなMutationがあればここに
},
actions: {
// グローバルなActionがあればここに
},
modules: {
// ‘todos’ という名前空間でtodosModuleを登録
todos: todosModule
}
});
export default store;
“`
4. コンポーネントの実装
Vuexストアと連携するコンポーネントを作成します。mapState
, mapGetters
, mapActions
ヘルパー関数を使って、ストアの要素をコンポーネントにマッピングします。
Todoリストコンポーネント (src/components/TodoList.vue
)
“`vue
Todo List
完了したTodo: {{ doneTodosCount }} 件
全てのTodo
-
{{ todo.text }}
完了済みTodo
- {{ todo.text }}
未完了Todo
- {{ todo.text }}
“`
App.vue で TodoList コンポーネントを表示:
“`vue
“`
これで、Vuexを使ったTodoアプリケーションが完成しました。この例では、以下のVuexの概念が使われています。
- State:
todos
配列 - Getters:
allTodos
,doneTodos
,pendingTodos
,doneTodosCount
- Mutations:
ADD_TODO
,TOGGLE_TODO
,REMOVE_TODO
- Actions:
addTodo
,toggleTodo
,removeTodo
,fetchTodos
- Modules:
todos
モジュール(名前空間付き) - Helper Functions:
mapState
,mapGetters
,mapActions
これにより、Todoに関する全ての状態とロジックがVuexストアの todos
モジュールに集約され、コンポーネントはStateを参照し、Actionをディスパッチする責務に集中できるようになります。
Vuexの応用的なトピック
Vuexの基本的な使い方を理解したところで、いくつか応用的なトピックに触れてみましょう。
1. Strict Mode
Strict Modeを有効にすると、Mutationハンドラ以外でStateが変更された場合にエラーが発生します。これは、開発中に予期しないStateの変更(Mutationを介さない直接的な変更)を検出し、デバッグを容易にするための強力な機能です。
Strict Modeの有効化:
ストアを作成する際に strict: true
オプションを追加します。
“`javascript
// src/store/index.js
import { createStore } from ‘vuex’;
import todosModule from ‘./modules/todos’;
const store = createStore({
// … state, getters, mutations, actions
modules: {
todos: todosModule
},
strict: process.env.NODE_ENV !== ‘production’ // 開発環境でのみ有効にするのが一般的
});
export default store;
“`
process.env.NODE_ENV !== 'production'
とすることで、本番環境ではパフォーマンスのためにStrict Modeを無効にできます。
注意点:
- Strict Modeは開発環境でのみ使用してください。本番環境で有効にすると、パフォーマンスに影響を与える可能性があります。
- 非同期処理内でStateを直接変更しようとするとエラーになります。必ずMutationを介してStateを変更してください。
2. プラグイン (Plugins)
Vuexストアには、プラグインを適用することができます。Vuexプラグインは、ストアの作成時にフックされ、ストアのライフサイクルイベント(Mutationのコミットなど)に反応したり、ストアに機能を追加したりできます。
一般的なプラグインの用途としては、以下のようなものがあります。
- 状態の永続化 (Persistence): ストアの状態をローカルストレージなどに保存し、アプリケーション再起動時に復元する。
vuex-persistedstate
のようなライブラリがよく使われます。 - ロギング (Logging): 全てのMutationやActionの実行をコンソールに出力する。Vuexに組み込みのロギングプラグインもあります(ただし非推奨になりつつあります)。
- カスタム機能の追加: ストアのインスタンスに新しいメソッドを追加するなど。
簡単なプラグインの例:
プラグインは引数としてストアインスタンスを受け取る関数です。
“`javascript
// myPlugin.js
const myPlugin = (store) => {
// ストアが初期化されたときに実行
console.log(‘Vuex store initialized!’, store.state);
// Mutationがコミットされるたびに実行
store.subscribe((mutation, state) => {
console.log(‘Mutation committed:’, mutation.type, mutation.payload);
console.log(‘Current state:’, state);
});
// Actionがディスパッチされるたびに実行
store.subscribeAction((action, state) => {
console.log(‘Action dispatched:’, action.type, action.payload);
// Action完了後の処理などもここに記述できる
});
};
export default myPlugin;
“`
プラグインの適用:
ストアを作成する際に plugins
オプションに配列として指定します。
“`javascript
// src/store/index.js
import { createStore } from ‘vuex’;
import todosModule from ‘./modules/todos’;
import myPlugin from ‘./myPlugin’; // 作成したプラグインをインポート
const store = createStore({
// … state, getters, mutations, actions
modules: {
todos: todosModule
},
plugins: [myPlugin] // プラグインを適用
});
export default store;
“`
3. テスト (Testing)
Vuexストアのテストは、アプリケーションのロジックが正しく動作することを保証する上で非常に重要です。Vuexのテストは比較的容易に行えます。
- State: 単なるオブジェクトなので、直接テストできます。
- Getters: Stateを引数に取る純粋関数なので、Stateのモックデータを使ってテストできます。
- Mutations: Stateとペイロードを引数に取る純粋関数なので、Stateのモックデータとペイロードを使ってテストできます。Stateが正しく変更されるかを確認します。
- Actions:
context
オブジェクトとペイロードを引数に取ります。context
をモックし、context.commit
やcontext.dispatch
が正しい引数で呼び出されるか、または非同期処理の結果 State が正しく変更されるかをテストします。
テストの例 (Jestを使用):
“`javascript
// tests/unit/store/modules/todos.spec.js
import todosModule from ‘@/store/modules/todos’; // モジュールをインポート
describe(‘todos Module’, () => {
let state;
// 各テストの前にStateをリセット
beforeEach(() => {
state = todosModule.state(); // 初期Stateを取得
});
// Gettersのテスト
describe(‘getters’, () => {
it(‘doneTodosCount should return the count of done todos’, () => {
state.todos = [
{ id: ‘1’, text: ‘Todo 1’, done: true },
{ id: ‘2’, text: ‘Todo 2’, done: false },
{ id: ‘3’, text: ‘Todo 3’, done: true },
];
const getters = todosModule.getters;
// getters.doneTodosCount は第二引数に他のgetter (doneTodos) を取るため、それをモックする必要がある
const localGetters = {
doneTodos: getters.doneTodos(state)
};
expect(getters.doneTodosCount(state, localGetters)).toBe(2);
});
it('getTodoById should return the todo with the given ID', () => {
state.todos = [
{ id: 'a', text: 'Todo A', done: false },
{ id: 'b', text: 'Todo B', done: true },
];
const getters = todosModule.getters;
expect(getters.getTodoById(state)('a')).toEqual({ id: 'a', text: 'Todo A', done: false });
expect(getters.getTodoById(state)('c')).toBeUndefined();
});
});
// Mutationsのテスト
describe(‘mutations’, () => {
it(‘ADD_TODO should add a new todo’, () => {
const text = ‘New Todo’;
todosModule.mutations.ADD_TODO(state, text); // Mutationを直接呼び出す
expect(state.todos.length).toBe(1);
expect(state.todos[0].text).toBe(text);
expect(state.todos[0].done).toBe(false);
expect(state.todos[0]).toHaveProperty(‘id’); // IDが生成されているか
});
it('TOGGLE_TODO should toggle the done status of a todo', () => {
state.todos = [{ id: '1', text: 'Toggle Me', done: false }];
todosModule.mutations.TOGGLE_TODO(state, '1');
expect(state.todos[0].done).toBe(true);
todosModule.mutations.TOGGLE_TODO(state, '1');
expect(state.todos[0].done).toBe(false);
});
it('REMOVE_TODO should remove a todo by id', () => {
state.todos = [
{ id: '1', text: 'Keep Me', done: false },
{ id: '2', text: 'Remove Me', done: false },
];
todosModule.mutations.REMOVE_TODO(state, '2');
expect(state.todos.length).toBe(1);
expect(state.todos[0].id).toBe('1');
});
});
// Actionsのテスト
describe(‘actions’, () => {
it(‘addTodo should commit ADD_TODO mutation’, () => {
const commit = jest.fn(); // commit関数をモック
const text = ‘Test Todo’;
todosModule.actions.addTodo({ commit }, text); // Actionを呼び出す
expect(commit).toHaveBeenCalledWith(‘ADD_TODO’, text); // commitが正しく呼ばれたか確認
});
it('fetchTodos should commit ADD_TODO for each dummy todo', async () => {
const commit = jest.fn();
// fetchTodos Actionは非同期の可能性があるのでasync/awaitを使う
await todosModule.actions.fetchTodos({ commit });
// ダミーデータが3件なので、ADD_TODOが3回コミットされることを期待
expect(commit).toHaveBeenCalledTimes(3);
// 具体的な引数を確認することも可能 (必要に応じて)
// expect(commit).toHaveBeenCalledWith('ADD_TODO', expect.any(String)); // テキストが文字列であること
});
});
});
“`
Vuexの要素は比較的独立してテストできるため、単体テストに適しています。これにより、ストアのロジックが期待通りに動作することを自信を持って開発を進めることができます。
Vuexのベストプラクティスと注意点
Vuexを効果的に使うためのベストプラクティスと、注意すべき点をいくつかご紹介します。
- Stateを直接変更しない: これはVuexの最も基本的なルールです。必ずMutationを介してStateを変更してください。Strict Modeを使うことで、このルールを破った場合に検知できます。
- Mutationは同期的に保つ: Mutation内で非同期処理(setTimeout, APIコールなど)を行わないでください。非同期処理はActionsで行い、その結果に基づいてMutationをコミットしてください。
- Actionsは非同期処理や複雑なロジックに使う: APIコール、複数のMutationの組み合わせ、他のActionの呼び出しなどはActionsで行います。
- GettersはStateの派生データに使う: 元のStateから計算される値はGetterとして定義することで、コードの重複を防ぎ、可読性を高めます。
- 大規模なアプリケーションではModulesを使う: アプリケーションが大きくなってきたら、機能やドメインごとにストアをModuleに分割しましょう。名前空間付きModule (
namespaced: true
) を使うことで、名前の衝突を防ぎ、コードを整理できます。 - ストア構造の設計: アプリケーションの規模や性質に応じて、ストアのState構造(どのようなデータをどのように保持するか)、Getter, Mutation, Actionの役割分担を事前に設計することが重要です。最初から完璧を目指す必要はありませんが、開発を進める中でリファクタリングを検討しましょう。
- コンポーネントローカルな状態とVuexストアの状態を区別する: 全てのデータをVuexストアに入れる必要はありません。そのコンポーネント自身だけが使用し、他のコンポーネントと共有する必要のない状態(例: 入力フィールドの値、ドロップダウンの開閉状態など)は、コンポーネントの
data
やローカルのref
/reactive
で管理する方がシンプルです。共有が必要なデータや、複数のコンポーネントから変更される可能性があるデータはVuexストアに置きましょう。 - Vuex Devtoolsを活用する: Vuex Devtoolsブラウザ拡張機能は、ストアの状態をリアルタイムで監視したり、MutationやActionの履歴を確認したり(タイムトラベルデバッグ)、Stateのスナップショットを取ったりするのに非常に役立ちます。開発効率を大幅に向上させるので、必ず利用しましょう。
- ペイロードにはオブジェクトを使うことを検討する: MutationやActionに複数の値を渡す場合、ペイロードをオブジェクト形式にすると、引数の順序を気にしなくて済み、コードの意図が分かりやすくなります。
まとめ
Vuexは、Vue.jsアプリケーションにおける複雑な状態管理の課題を解決するための強力なライブラリです。その核となる概念であるState, Getters, Mutations, Actions, Modulesを理解し、適切に使うことで、以下のようなメリットが得られます。
- 状態のセントラライズ化: アプリケーション全体の共有状態が一箇所に集約され、管理しやすくなる。
- 予測可能な状態変更: 状態の変更がMutationという明確なルールに従ってのみ行われるため、データフローが追跡しやすくなる。
- デバッグの容易さ: Vuex Devtoolsを使ったタイムトラベルデバッグが可能になり、問題の原因特定が容易になる。
- コンポーネント間の疎結合: コンポーネントは直接データをやり取りするのではなく、ストアを介して状態を共有するため、依存関係が減り、再利用性が高まる。
- 保守性の向上: 状態管理のロジックがストアにカプセル化されるため、コードの整理と保守が容易になる。
小規模なアプリケーションであれば、Vuexはオーバースペックかもしれません。しかし、アプリケーションが大きくなり、多くのコンポーネント間で複雑な状態を共有・操作する必要が出てきた場合には、Vuexの導入を強く検討すべきです。
この記事では、Vuexの基本から実践的なTodoアプリの例、そして応用的なトピックまでを幅広く解説しました。これを機に、ぜひあなたのVue.jsアプリケーション開発にVuexを取り入れてみてください。予測可能で保守しやすい状態管理を実現し、開発体験が大きく向上するはずです。
(この時点で約5000語です。)