Vuex/Piniaのステート、ゲッター、ミューテーションの基本:状態管理の核心概念を徹底解説

2025-07-31

はじめに

VuexやPiniaを使用した状態管理の核心となる「ステート(State)」「ゲッター(Getters)」「ミューテーション(Mutations)」の3つの概念は、効果的な状態管理を実現するための基礎です。本記事では、これらの概念を詳細に解説し、具体的なコード例と図解を交えてその役割と使用方法を説明します。Vuexを主な例として使用しますが、Piniaとの違いについても随時触れていきます。

ステート(State):アプリケーションの状態を保持する

ステートとは?

ステートはアプリケーション全体の状態を保持するオブジェクトで、単一の信頼できる情報源(Single Source of Truth)として機能します。全てのコンポーネントが参照可能なグローバルなデータストアと考えることができます。

state

ステートの定義方法(Vuex)

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    user: {
      id: null,
      name: '',
      email: ''
    },
    todos: []
  }
})

ステートの定義方法(Pinia)

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: null,
    todos: []
  })
})

コンポーネントでのステートの使用方法

<!-- Vuexを使用したコンポーネント -->
<template>
  <div>
    <p>カウント: {{ count }}</p>
    <p>ユーザー名: {{ user.name }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['count', 'user'])

    // またはオブジェクト形式でマッピング
    // ...mapState({
    //   count: state => state.count,
    //   userName: state => state.user.name
    // })
  }
}
</script>
<!-- Piniaを使用したコンポーネント -->
<template>
  <div>
    <p>カウント: {{ counter.count }}</p>
    <p>ユーザー名: {{ counter.user?.name }}</p>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

ステートの特徴

  1. リアクティブ: Vueのリアクティブシステムと統合されており、状態が変更されると自動的にビューが更新される
  2. 直接変更不可(Vuex): Vuexではミューテーションを介さずに直接ステートを変更すべきではない
  3. 直接変更可能(Pinia): Piniaではストア内で直接ステートを変更できる(ただし、可能な場合はアクションを使用することが推奨される)

ゲッター(Getters):ステートから派生した値を計算

ゲッターとは?

ゲッターはステートから派生した値を計算するための関数で、コンポーネントのcomputedプロパティに似ています。同じ計算ロジックが複数のコンポーネントで必要な場合や、複雑な状態のフィルタリング・変換が必要な場合に特に有用です。

ゲッターの定義方法(Vuex)

export default new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: 'Vue.jsを学ぶ', done: true },
      { id: 2, text: 'Vuexを理解する', done: false },
      { id: 3, text: 'Piniaを試す', done: false }
    ]
  },
  getters: {
    // 完了したTodoのみを返す
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    // 特定のIDのTodoを取得
    getTodoById: state => id => {
      return state.todos.find(todo => todo.id === id)
    },
    // 未完了のTodoの数を返す
    undoneTodosCount: (state, getters) => {
      return state.todos.length - getters.doneTodos.length
    }
  }
})

ゲッターの定義方法(Pinia)

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [
      { id: 1, text: 'Vue.jsを学ぶ', done: true },
      { id: 2, text: 'Vuexを理解する', done: false },
      { id: 3, text: 'Piniaを試す', done: false }
    ]
  }),
  getters: {
    doneTodos(state) {
      return state.todos.filter(todo => todo.done)
    },
    getTodoById: state => {
      return id => state.todos.find(todo => todo.id === id)
    },
    undoneTodosCount(state) {
      return this.todos.length - this.doneTodos.length
    }
  }
})

コンポーネントでのゲッターの使用方法

<!-- Vuexを使用したコンポーネント -->
<template>
  <div>
    <h3>完了したタスク</h3>
    <ul>
      <li v-for="todo in doneTodos" :key="todo.id">
        {{ todo.text }}
      </li>
    </ul>
    <p>未完了のタスク数: {{ undoneTodosCount }}</p>
    <p>ID2のタスク: {{ getTodo(2).text }}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      'doneTodos',
      'undoneTodosCount',
      'getTodoById'
    ]),
    getTodo() {
      return this.getTodoById
    }
  }
}
</script>
<!-- Piniaを使用したコンポーネント -->
<template>
  <div>
    <h3>完了したタスク</h3>
    <ul>
      <li v-for="todo in todoStore.doneTodos" :key="todo.id">
        {{ todo.text }}
      </li>
    </ul>
    <p>未完了のタスク数: {{ todoStore.undoneTodosCount }}</p>
    <p>ID2のタスク: {{ todoStore.getTodoById(2).text }}</p>
  </div>
</template>

<script setup>
import { useTodoStore } from '@/stores/todo'

const todoStore = useTodoStore()
</script>

ゲッターの特徴

  1. キャッシュされる: 依存するステートが変更されない限り、再計算されない
  2. 引数を取れる: メソッドスタイルのゲッターを使用することで引数を渡せる
  3. 他のゲッターを利用可能: ゲッター内で他のゲッターを参照できる
  4. コンポーネントの計算プロパティとの類似: ロジックの再利用と関心の分離が可能

ミューテーション(Mutations):ステートを変更する唯一の方法

ミューテーションとは?

ミューテーションはVuexにおいてステートを変更する唯一の方法です。各ミューテーションは文字列のタイプ(type)とハンドラ関数を持ち、同期的にステートを変更します。

vuex

ミューテーションの定義方法(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    user: null
  },
  mutations: {
    // ペイロードなしのミューテーション
    increment(state) {
      state.count++
    },
    // ペイロードありのミューテーション
    setUser(state, user) {
      state.user = user
    },
    // オブジェクトスタイルのペイロード
    updateProfile(state, { name, email }) {
      if (state.user) {
        state.user.name = name
        state.user.email = email
      }
    }
  }
})

ミューテーションの呼び出し方法(Vuex)

ミューテーションは直接呼び出すことはできず、commitメソッドを使用して呼び出します。

<!-- Vuexを使用したコンポーネント -->
<template>
  <div>
    <p>カウント: {{ count }}</p>
    <button @click="increment">増やす</button>
    <button @click="updateUser">ユーザーを更新</button>
  </div>
</template>

<script>
import { mapState, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapState(['count', 'user'])
  },
  methods: {
    ...mapMutations(['increment', 'setUser']),
    updateUser() {
      this.setUser({ name: '新しいユーザー', email: 'new@example.com' })
      // または直接コミット
      // this.$store.commit('setUser', { name: '新しいユーザー', email: 'new@example.com' })
    }
  }
}
</script>

Piniaにおけるミューテーションの代替

Piniaにはミューテーションの概念がなく、アクション(Actions)で直接ステートを変更できますが、Vuexのミューテーションに相当するパターンを実装することは可能です。

// Piniaでのミューテーション的パターン
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    // ミューテーション的なアクション
    increment() {
      this.count++
    },
    setUser(user) {
      this.user = user
    }
  }
})

ミューテーションの特徴

  1. 同期的である必要がある: 非同期処理はアクションで行う
  2. 直接呼び出せない: commitを使用して呼び出す
  3. 追跡可能: Vue DevToolsでミューテーションの記録を確認できる
  4. ペイロードを受け取れる: ミューテーションにデータを渡せる

3つの概念の連携フロー

ステート、ゲッター、ミューテーションの関係を理解するために、典型的なデータフローを見てみましょう。

コンポーネント → (ディスパッチ) → アクション → (コミット) → ミューテーション → (変更) → ステート → (反映) → ゲッター → (更新) → コンポーネント

具体的なフローの例

  1. ユーザーがボタンをクリック
  2. コンポーネントがアクションをディスパッチ
  3. アクションがAPIからデータを取得
  4. アクションがミューテーションをコミット
  5. ミューテーションがステートを更新
  6. ステートの変更がゲッターに反映
  7. ゲッターの変更がコンポーネントの表示を更新

ベストプラクティス

ステート設計のコツ

  1. 適切な構造化: 関連するデータはグループ化する
  2. 正規化: 複雑なデータ関係はデータベースのように正規化
  3. シリアライズ可能: デバッグのためにシリアライズ可能なデータを保持

ゲッターの活用方法

  1. 複雑なロジックのカプセル化: コンポーネントから複雑な状態計算を隠蔽
  2. 再利用: 複数のコンポーネントで同じ計算が必要な場合に使用
  3. パフォーマンス最適化: 計算量の多い処理をキャッシュ

ミューテーションの設計原則

  1. 単一責任: 1つのミューテーションは1つの変更だけを行う
  2. 予測可能: 同じ入力に対して常に同じ結果を返す
  3. シンプルに: 複雑なロジックはアクションに任せる

まとめ

Vuex/Piniaの状態管理システムの中核をなすステート、ゲッター、ミューテーションの3つの概念について詳しく解説しました。これらの概念を正しく理解し、適切に活用することで、以下のようなメリットが得られます。

  • ステート: アプリケーションの「真実の源」として、データを一元的に管理
  • ゲッター: 派生状態を効率的に計算し、コンポーネント間でロジックを共有
  • ミューテーション: 状態変更を追跡可能な方法で制御(Vuex)

これらの概念を組み合わせることで、大規模なVue.jsアプリケーションでもデータフローを明確に保ち、保守性の高いコードベースを維持できます。VuexとPiniaには違いがありますが、基本的な考え方は共通しています。プロジェクトの要件に合わせて適切なライブラリを選択し、これらの状態管理の基本概念を活用してください。