はじめに (対象読者・この記事でわかること)

この記事は、Vue.jsとVuexの基本的な概念を理解しているが、mutationsとactionsの違いが曖昧で、適切に使い分けができていない開発者を対象としています。特に、状態管理のベストプラクティスを学びたい方に最適です。

この記事を読むことで、mutationsとactionsの役割の違いが明確に理解できるようになります。また、非同期処理の扱い方、実際のアプリケーションでの使い分け方、そしてベストプラクティスを習得し、より効率的な状態管理ができるようになります。具体的なコード例を交えて解説するので、実践的な知識を身につけることができます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Vue.jsの基本的な知識
  • Vuexの基本概念(store, state, gettersなど)の理解

Vuexの状態管理とmutations・actionsの必要性

VuexはVue.jsのための状態管理パターンおよびライブラリです。大規模なアプリケーションにおいて、コンポーネント間の状態を予測可能に管理するために設計されています。Vuexの核となる概念は、state(状態)、mutations(突然変異)、actions(アクション)、getters(ゲッター)の4つです。

特にmutationsとactionsは、状態の変更に関連する重要な概念ですが、その役割と使い方が混同されがちです。多くの開発者が「非同期処理はactions、同期処理はmutations」という基本的な理解は持っていても、実際の開発現場では両者の役割が明確に分かれておらず、可読性や保守性の低いコードが書かれてしまうことがあります。

なぜmutationsとactionsという2つの概念が必要なのでしょうか?これは、状態変更の追跡可能性とデバッグのしやすさを確保するためです。Vuexは、状態の変更をmutationsという「コミット」を介して行うことで、状態の変化を追跡しやすくしています。一方、actionsはmutationsをコミットする前の処理(API呼び出しなどの非同期処理)を担当し、アプリケーションのロジックをクリーンに保つ役割を担っています。

mutationsとactionsの詳細と使い分け

mutationsの詳細と使い方

mutationsは、状態を変更するための唯一の方法です。Vuexの重要なルールとして「状態の変更は必ずmutationsを通じて行う」というものがあります。mutationsは同期処理である必要があり、直接状態を変更する処理を記述します。

mutationsの基本的な構造は以下のようになります。

Javascript
// store.js const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment(state) { state.count++ }, decrement(state) { state.count-- }, incrementBy(state, payload) { state.count += payload.amount } } })

mutationsを呼び出す(コミットする)方法は2つあります。1つは直接コミットする方法、もう1つはタイプ(文字列)でコミットする方法です。

Javascript
// 直接コミット store.commit('increment') // タイプとペイロードを指定してコミット store.commit({ type: 'incrementBy', amount: 10 })

mutationsの重要な特徴は以下の通りです。

  1. 同期処理である必要がある
  2. 第1引数にはstateが渡される
  3. 第2引数にはペイロード(任意の追加引数)を渡せる
  4. 状態を直接変更する

actionsの詳細と使い方

actionsはmutationsをコミットする前の処理を担当します。主な用途は非同期処理(API呼び出しなど)ですが、同期処理も行えます。actionsはmutationsと異なり、任意の非同期処理を含むことができます。

actionsの基本的な構造は以下のようになります。

Javascript
// store.js const store = new Vuex.Store({ state: { users: [] }, mutations: { setUsers(state, users) { state.users = users } }, actions: { fetchUsers({ commit }) { // API呼び出しなどの非同期処理 return axios.get('/api/users') .then(response => { // 成功したらmutationsをコミット commit('setUsers', response.data) }) .catch(error => { // エラー処理 console.error('Error fetching users:', error) }) }, // 非同期処理を伴わないアクションの例 logAction({ dispatch }, message) { console.log('Action dispatched:', message) // 他のアクションをディスパッチすることも可能 dispatch('anotherAction') } } })

actionsを呼び出す(ディスパッチする)方法は以下の通りです。

Javascript
// アクションをディスパッチ store.dispatch('fetchUsers') // ペイロードを指定してディスパッチ store.dispatch('fetchUsers', { userId: 123 }) // オブジェクト形式でディスパッチ store.dispatch({ type: 'fetchUsers', userId: 123 })

actionsの重要な特徴は以下の通りです。

  1. 非同期処理を含むことができる
  2. 第1引数にはcontextオブジェクトが渡される(state, commit, dispatchなどが含まれる)
  3. mutationsをコミットする
  4. 他のactionsをディスパッチできる
  5. Promiseを返すことができる

具体的なコード例を交えた比較

以下に、ユーザー情報を取得して表示するシナリオを例に、mutationsとactionsの使い分けを具体的に示します。

mutationsのみの場合(非推奨)

Javascript
const store = new Vuex.Store({ state: { users: [], loading: false, error: null }, mutations: { setUsers(state, users) { state.users = users }, setLoading(state, isLoading) { state.loading = isLoading }, setError(state, error) { state.error = error } } }) // コンポーネント内で直接API呼び出しを行う(非推奨) methods: { fetchUsers() { this.$store.commit('setLoading', true) axios.get('/api/users') .then(response => { this.$store.commit('setUsers', response.data) }) .catch(error => { this.$store.commit('setError', error.message) }) .finally(() => { this.$store.commit('setLoading', false) }) } }

この方法では、状態管理のロジックがコンポーネントに散らばり、再利用性が低くなります。また、テストも困難になります。

actionsを使用した推奨される方法

Javascript
const store = new Vuex.Store({ state: { users: [], loading: false, error: null }, mutations: { setUsers(state, users) { state.users = users }, setLoading(state, isLoading) { state.loading = isLoading }, setError(state, error) { state.error = error } }, actions: { fetchUsers({ commit }) { commit('setLoading', true) commit('setError', null) return axios.get('/api/users') .then(response => { commit('setUsers', response.data) }) .catch(error => { commit('setError', error.message) throw error // エラーを呼び出し元に伝える }) .finally(() => { commit('setLoading', false) }) } } }) // コンポーネント内でアクションをディスpatch methods: { async fetchUsers() { try { await this.$store.dispatch('fetchUsers') } catch (error) { // エラーハンドリング } } }

この方法では、API呼び出しのロジックがstoreに集約され、コンポーネントは状態の変更方法ではなく「何をしたいか」に集中できます。また、アクションはPromiseを返すため、非同期処理の結果を簡単に扱えます。

非同期処理の扱い方

Vuexでは、非同期処理はactionsで扱うのが基本です。mutationsは同期処理である必要があるため、API呼び出しなどの非同期処理を直接mutationsに記述することはできません。

以下に、非同期処理を扱う際のベストプラクティスを示します。

非同期処理を含むアクションの例

Javascript
actions: { // ユーザー情報を取得するアクション fetchUser({ commit }, userId) { return new Promise((resolve, reject) => { axios.get(`/api/users/${userId}`) .then(response => { commit('setUser', response.data) resolve(response.data) }) .catch(error => { commit('setError', error.message) reject(error) }) }) }, // 複数の非同期処理を待つアクション fetchUserData({ dispatch }, userId) { return Promise.all([ dispatch('fetchUser', userId), dispatch('fetchUserPosts', userId), dispatch('fetchUserComments', userId) ]) } }

コンポーネントでの非同期処理の扱い

Javascript
export default { methods: { async loadUser() { this.loading = true try { await this.$store.dispatch('fetchUser', this.userId) // 成功時の処理 } catch (error) { // エラー処理 this.errorMessage = 'ユーザー情報の取得に失敗しました' } finally { this.loading = false } } }, created() { this.loadUser() } }

実践的なユースケース

以下に、実際のアプリケーションでよくあるユースケースと、その場合のmutationsとactionsの使い分けを示します。

ユーザー登録フォーム

Javascript
// store.js const store = new Vuex.Store({ state: { user: null, registrationStatus: null }, mutations: { setUser(state, user) { state.user = user }, setRegistrationStatus(state, status) { state.registrationStatus = status } }, actions: { registerUser({ commit }, userData) { commit('setRegistrationStatus', 'pending') return axios.post('/api/register', userData) .then(response => { commit('setUser', response.data.user) commit('setRegistrationStatus', 'success') return response.data.user }) .catch(error => { commit('setRegistrationStatus', 'error') throw error }) } } }) // コンポーネント export default { data() { return { formData: { name: '', email: '', password: '' }, submitting: false } }, methods: { async submitForm() { this.submitting = true try { await this.$store.dispatch('registerUser', this.formData) // 成功時の処理(リダイレクトなど) this.$router.push('/dashboard') } catch (error) { // エラー処理 this.errorMessage = '登録に失敗しました' } finally { this.submitting = false } } } }

複数のAPI呼び出しを待つ処理

Javascript
// store.js const store = new Vuex.Store({ state: { dashboardData: null, loading: false }, mutations: { setDashboardData(state, data) { state.dashboardData = data }, setLoading(state, isLoading) { state.loading = isLoading } }, actions: { loadDashboard({ dispatch }) { this.commit('setLoading', true) return Promise.all([ dispatch('fetchStats'), dispatch('fetchRecentActivities'), dispatch('fetchNotifications') ]).then(() => { this.commit('setLoading', false) }) }, fetchStats({ commit }) { return axios.get('/api/stats') .then(response => { commit('setStats', response.data) }) }, fetchRecentActivities({ commit }) { return axios.get('/api/activities') .then(response => { commit('setActivities', response.data) }) }, fetchNotifications({ commit }) { return axios.get('/api/notifications') .then(response => { commit('setNotifications', response.data) }) } } })

ハマりやすいポイントと解決策

ポイント1: mutationsで非同期処理を行おうとする

Javascript
// 誤った例 mutations: { fetchData(state) { axios.get('/api/data') // mutationsで非同期処理を行う .then(response => { state.data = response.data }) } }

解決策: 非同期処理はactionsで行い、mutationsは同期処理に限定します。

Javascript
// 正しい例 actions: { fetchData({ commit }) { return axios.get('/api/data') .then(response => { commit('setData', response.data) }) } }, mutations: { setData(state, data) { state.data = data } }

ポイント2: コンポーネントで直接stateを変更する

Javascript
// 誤った例 methods: { updateCount() { this.$store.state.count++ // 直接stateを変更する } }

解決策: 状態の変更は必ずmutationsを通じて行います。

Javascript
// 正しい例 methods: { updateCount() { this.$store.commit('increment') } }

ポイント3: actions内でstateを直接変更する

Javascript
// 誤った例 actions: { fetchData({ state }) { axios.get('/api/data') .then(response => { state.data = response.data // actionsで直接stateを変更する }) } }

解決策: actions内でも状態の変更はmutationsを通じて行います。

Javascript
// 正しい例 actions: { fetchData({ commit }) { return axios.get('/api/data') .then(response => { commit('setData', response.data) }) } }

ポイント4: 非同期処理のエラーハンドリングを忘れる

Javascript
// 誤った例 actions: { fetchData({ commit }) { axios.get('/api/data') .then(response => { commit('setData', response.data) }) // .catchを忘れている } }

解決策: 非同期処理では必ずエラーハンドリングを行います。

Javascript
// 正しい例 actions: { fetchData({ commit }) { return axios.get('/api/data') .then(response => { commit('setData', response.data) }) .catch(error => { commit('setError', error.message) throw error // エラーを呼び出し元に伝える }) } }

まとめ

本記事では、Vuexのmutationsとactionsの違いと使い分けについて解説しました。

  • mutationsは同期処理のみで、状態を直接変更する役割を持っています。第1引数にはstateが渡され、コミットによって呼び出されます。
  • actionsは非同期処理を含むことができ、mutationsをコミットする前の処理を担当します。第1引数にはcontextオブジェクトが渡され、ディスパッチによって呼び出されます。
  • 状態の変更は必ずmutationsを通じて行い、非同期処理はactionsで行うという役割分担を守ることが重要です。
  • 実践的なユースケースでは、API呼び出しなどの非同期処理をactionsで処理し、その結果をmutationsで状態に反映させる流れが基本となります。
  • ハマりやすいポイントとして、mutationsで非同期処理を行おとする、直接stateを変更する、エラーハンドリングを忘れるなどがありますが、これらを避けることでより安定した状態管理が可能になります。

この記事を通して、Vuexの状態管理をより効果的に行う方法が理解できたことと思います。mutationsとactionsの役割を明確に理解し、適切に使い分けることで、保守性の高いコードを書くことができるようになります。

今後は、モジュール化による大規模アプリケーションでの状態管理や、Vuexの代替となるPiniaなどの新しい状態管理ライブラリについても記事にする予定です。

参考資料