第9章: Pinia で状態管理

第9章: Pinia で状態管理

概要

本章では、Vue.jsアプリケーションにおける状態管理の概念と、Piniaを使用した効果的な状態管理の方法について学びます。状態管理の必要性、Piniaの基本的な使い方、ストアの設計パターン、そしてVue.jsのリアクティブシステムとの統合について理解を深めます。

1. 状態管理の概念

まず、アプリケーションの状態管理とは何か、なぜ必要なのかを理解しましょう。

アプリケーションの状態とは

アプリケーションの「状態」とは、アプリケーションが動作するために必要なデータを指します。例えば、ユーザー情報、製品リスト、現在の表示モード、フォームの入力値などがアプリケーションの状態に含まれます。

状態管理の問題

小規模なアプリケーションでは、コンポーネント内でローカルの状態(refreactive)を使用して管理することができます。しかし、アプリケーションが大きくなるにつれて、以下のような問題が発生します:

Props Drilling問題の図

状態管理パターン

これらの問題を解決するために、「状態管理パターン」が生まれました。状態管理パターンは、以下の要素で構成されます:

状態管理パターンでは、状態の変更は一方向のデータフローに従います:ビューはアクションをトリガーし、アクションは状態を変更し、状態の変更がビューに反映されます。

状態管理パターンの図

状態管理ライブラリの必要性

状態管理パターンを実装するために、専用のライブラリを使用することが一般的です。Vue.jsエコシステムでは、以下のような状態管理ライブラリがあります:

2. Piniaの登場と特徴

Piniaは、Vue 3のために設計された次世代の状態管理ライブラリです。当初はVuexの代替として開発されましたが、現在はVue.jsの公式状態管理ライブラリとして採用されています。

なぜPiniaが登場したのか

Vuexには以下のような問題点がありました:

Piniaの主な特徴

Piniaは、これらの問題を解決するために設計されました:

Piniaのモジュラー設計

3. Piniaのインストールと設定

Piniaを使い始めるには、まずインストールと基本設定が必要です。

インストール

npm install pinia

Vueアプリケーションへの統合

// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

4. Piniaストアの基本

Piniaでは、「ストア」はアプリケーションの状態を保持し、操作するためのオブジェクトです。

ストアの定義

Piniaストアは、defineStore関数を使用して定義します。

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

// ストアの定義(IDと設定オブジェクト)
export const useCounterStore = defineStore('counter', {
  // 状態(State)
  state: () => ({
    count: 0,
    name: 'カウンター'
  }),
  
  // ゲッター(Getters)
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // 他のゲッターを使用するゲッター
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  
  // アクション(Actions)
  actions: {
    increment() {
      this.count++
    },
    
    decrement() {
      this.count--
    },
    
    // 非同期アクション
    async fetchCount() {
      try {
        const response = await fetch('/api/count')
        const data = await response.json()
        this.count = data.count
      } catch (error) {
        console.error('カウントの取得に失敗しました:', error)
      }
    }
  }
})

Composition API構文でのストア定義

Vue 3のComposition API構文を使用して、より簡潔にストアを定義することもできます。

// src/stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 状態(State)- refを使用
  const count = ref(0)
  const name = ref('カウンター')
  
  // ゲッター(Getters)- computedを使用
  const doubleCount = computed(() => count.value * 2)
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)
  
  // アクション(Actions)- 通常の関数を使用
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  async function fetchCount() {
    try {
      const response = await fetch('/api/count')
      const data = await response.json()
      count.value = data.count
    } catch (error) {
      console.error('カウントの取得に失敗しました:', error)
    }
  }
  
  // 公開するプロパティとメソッドを返す
  return {
    count,
    name,
    doubleCount,
    doubleCountPlusOne,
    increment,
    decrement,
    fetchCount
  }
})

5. ストアの使用

定義したストアを使用して、コンポーネント内で状態を管理する方法を見ていきましょう。

コンポーネント内でのストアの使用

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

// ストアのインスタンスを取得
const counterStore = useCounterStore()

// 状態、ゲッター、アクションにアクセス
console.log(counterStore.count)         // 状態へのアクセス
console.log(counterStore.doubleCount)   // ゲッターへのアクセス
counterStore.increment()                // アクションの実行
</script>

<template>
  <div>
    <h1>{{ counterStore.name }}</h1>
    <p>カウント: {{ counterStore.count }}</p>
    <p>2倍のカウント: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment()">増やす</button>
    <button @click="counterStore.decrement()">減らす</button>
  </div>
</template>

ストアの分解と自動参照のアンラップ

storeToRefs関数を使用すると、ストアのプロパティを分解しながらリアクティブ性を保持できます。

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

const counterStore = useCounterStore()

// storeToRefsを使用して状態とゲッターを分解
// アクションは含めない(リアクティブでないため)
const { count, name, doubleCount } = storeToRefs(counterStore)

// アクションは直接分解できる
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <h1>{{ name }}</h1>
    <p>カウント: {{ count }}</p>
    <p>2倍のカウント: {{ doubleCount }}</p>
    <button @click="increment()">増やす</button>
    <button @click="decrement()">減らす</button>
  </div>
</template>

外部コンポーネント間での状態共有

Piniaの主な利点の1つは、コンポーネント間で状態を簡単に共有できることです。

<!-- ComponentA.vue -->
<script setup>
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

function resetCounter() {
  counterStore.count = 0
}
</script>

<template>
  <div>
    <h2>コンポーネントA</h2>
    <p>カウント: {{ counterStore.count }}</p>
    <button @click="counterStore.increment()">A: 増やす</button>
    <button @click="resetCounter">リセット</button>
  </div>
</template>

<!-- ComponentB.vue -->
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
</script>

<template>
  <div>
    <h2>コンポーネントB</h2>
    <p>カウント: {{ count }}</p>
    <p>2倍のカウント: {{ doubleCount }}</p>
    <button @click="counterStore.increment()">B: 増やす</button>
  </div>
</template>

<!-- App.vue -->
<template>
  <div>
    <h1>Piniaによる状態共有</h1>
    <ComponentA />
    <ComponentB />
  </div>
</template>

上記の例では、ComponentAとComponentBが同じカウンターストアを使用しています。一方のコンポーネントで状態を変更すると、もう一方のコンポーネントでも変更が反映されます。

6. 状態の変更

Piniaでは、状態を変更する方法がいくつかあります。

直接変更

Vuexと異なり、Piniaでは状態を直接変更できます。

const counterStore = useCounterStore()

// 直接変更
counterStore.count = 10

// オブジェクトや配列のプロパティも直接変更可能
counterStore.user.name = '山田太郎'
counterStore.items[0] = '新しいアイテム'

アクションを使用した変更

状態の変更ロジックをカプセル化するために、アクションを使用することを推奨します。

// ストア定義
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    // 複雑なロジックをカプセル化
    incrementBy(amount) {
      if (amount <= 0) return
      this.count += amount
    },
    async fetchAndUpdate() {
      try {
        const response = await fetch('/api/data')
        const data = await response.json()
        this.count = data.newCount
      } catch (error) {
        console.error(error)
      }
    }
  }
})

// 使用
const counterStore = useCounterStore()
counterStore.incrementBy(5)
await counterStore.fetchAndUpdate()

$patchメソッド

複数の状態を一度に更新するには、$patchメソッドを使用できます。

const userStore = useUserStore()

// オブジェクトを使用して複数のプロパティを更新
userStore.$patch({
  name: '山田太郎',
  age: 25,
  isActive: true
})

// 関数を使用して複雑な更新を行う
userStore.$patch((state) => {
  state.count++
  state.history.push('カウントが増加: ' + state.count)
  state.lastUpdated = new Date()
})

$resetメソッド

ストアの状態を初期値にリセットするには、$resetメソッドを使用します。

const counterStore = useCounterStore()

// ストアの状態を初期値にリセット
counterStore.$reset()

7. ストア間の連携

複数のストアを組み合わせて使用する方法を見ていきましょう。

別のストアの使用

ストア内で他のストアを使用することができます。

// src/stores/cart.js
import { defineStore } from 'pinia'
import { useProductStore } from './product'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] // { productId, quantity }
  }),
  getters: {
    total() {
      const productStore = useProductStore()
      return this.items.reduce((sum, item) => {
        const product = productStore.getProductById(item.productId)
        return sum + (product?.price || 0) * item.quantity
      }, 0)
    }
  },
  actions: {
    addItem(productId, quantity = 1) {
      const productStore = useProductStore()
      const product = productStore.getProductById(productId)
      
      if (!product || product.stock < quantity) {
        return false
      }
      
      // カートに追加
      const existingItem = this.items.find(item => item.productId === productId)
      
      if (existingItem) {
        // 既存のアイテムの数量を更新
        existingItem.quantity += quantity
      } else {
        // 新しいアイテムを追加
        this.items.push({ productId, quantity })
      }
      
      // 商品の在庫を減らす
      productStore.decreaseStock(productId, quantity)
      
      return true
    },
    
    removeItem(productId) {
      const index = this.items.findIndex(item => item.productId === productId)
      if (index !== -1) {
        this.items.splice(index, 1)
      }
    }
  }
})

// src/stores/product.js
import { defineStore } from 'pinia'

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [
      { id: 1, name: '商品A', price: 100, stock: 10 },
      { id: 2, name: '商品B', price: 200, stock: 5 },
      { id: 3, name: '商品C', price: 300, stock: 3 }
    ]
  }),
  getters: {
    getProductById: (state) => {
      return (productId) => state.products.find(p => p.id === productId)
    },
    availableProducts: (state) => {
      return state.products.filter(p => p.stock > 0)
    }
  },
  actions: {
    decreaseStock(productId, quantity = 1) {
      const product = this.products.find(p => p.id === productId)
      if (product && product.stock >= quantity) {
        product.stock -= quantity
        return true
      }
      return false
    }
  }
})

コンポーズドストア

複雑なアプリケーションでは、コンポーザブル関数を使用して、複数のストアを組み合わせたカスタムロジックを作成できます。

// src/composables/useShoppingList.js
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/product'
import { useUserStore } from '@/stores/user'

export function useShoppingList() {
  const cartStore = useCartStore()
  const productStore = useProductStore()
  const userStore = useUserStore()
  
  // カートのアイテムを商品情報と一緒に取得
  const cartItems = computed(() => {
    return cartStore.items.map(item => {
      const product = productStore.getProductById(item.productId)
      return {
        ...item,
        product,
        total: (product?.price || 0) * item.quantity
      }
    })
  })
  
  // ユーザーの最大予算に基づいた購入可能なアイテム
  const affordableItems = computed(() => {
    const budget = userStore.currentUser?.budget || 0
    return cartItems.value.filter(item => item.total <= budget)
  })
  
  // 購入処理
  async function checkout() {
    if (cartStore.total > (userStore.currentUser?.budget || 0)) {
      throw new Error('予算が不足しています')
    }
    
    try {
      // 購入処理を実行
      await userStore.updateBudget(-cartStore.total)
      await cartStore.checkout()
      return true
    } catch (error) {
      console.error('購入処理に失敗しました:', error)
      return false
    }
  }
  
  return {
    cartItems,
    affordableItems,
    checkout
  }
}

8. Piniaを使った実践的な例

ここでは、Piniaを使ったTodoリストアプリケーションの例を見てみましょう。

Todoストアの定義

// src/stores/todo.js
import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [],
    filter: 'all', // 'all', 'completed', 'active'
    nextId: 0
  }),
  
  getters: {
    filteredTodos(state) {
      if (state.filter === 'completed') {
        return state.todos.filter(todo => todo.completed)
      } else if (state.filter === 'active') {
        return state.todos.filter(todo => !todo.completed)
      }
      return state.todos
    },
    
    totalCount: state => state.todos.length,
    
    completedCount: state => state.todos.filter(todo => todo.completed).length,
    
    activeCount: state => state.todos.filter(todo => !todo.completed).length,
    
    allCompleted: state => state.todos.length > 0 && state.todos.every(todo => todo.completed)
  },
  
  actions: {
    addTodo(text) {
      if (!text.trim()) return
      
      this.todos.push({
        id: this.nextId++,
        text,
        completed: false,
        createdAt: new Date()
      })
    },
    
    removeTodo(id) {
      const index = this.todos.findIndex(todo => todo.id === id)
      if (index !== -1) {
        this.todos.splice(index, 1)
      }
    },
    
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    
    setFilter(filter) {
      this.filter = filter
    },
    
    toggleAllTodos(completed = true) {
      this.todos.forEach(todo => {
        todo.completed = completed
      })
    },
    
    clearCompleted() {
      this.todos = this.todos.filter(todo => !todo.completed)
    },
    
    // 非同期アクション
    async fetchTodos() {
      try {
        const response = await fetch('/api/todos')
        const data = await response.json()
        
        // 既存のTodoをクリアして新しいデータをセット
        this.todos = data.map((todo, index) => ({
          id: index,
          text: todo.text,
          completed: todo.completed,
          createdAt: new Date(todo.createdAt)
        }))
        
        // 次のIDを更新
        this.nextId = this.todos.length
      } catch (error) {
        console.error('Todoの取得に失敗しました:', error)
      }
    }
  }
})

Todoリストコンポーネント

<!-- src/components/TodoList.vue -->
<script setup>
import { ref } from 'vue'
import { useTodoStore } from '@/stores/todo'
import { storeToRefs } from 'pinia'

const todoStore = useTodoStore()
const { filteredTodos, totalCount, completedCount, activeCount } = storeToRefs(todoStore)

const newTodoText = ref('')

function addTodo() {
  todoStore.addTodo(newTodoText.value)
  newTodoText.value = ''
}

function toggleAll() {
  todoStore.toggleAllTodos(!todoStore.allCompleted)
}
</script>

<template>
  <div class="todo-app">
    <h1>Todoリスト</h1>
    
    <!-- Todoフォーム -->
    <div class="todo-form">
      <input
        v-model="newTodoText"
        @keyup.enter="addTodo"
        placeholder="新しいタスクを入力..."
        type="text"
      >
      <button @click="addTodo">追加</button>
    </div>
    
    <!-- フィルター -->
    <div class="todo-filters">
      <button
        :class="{ active: todoStore.filter === 'all' }"
        @click="todoStore.setFilter('all')"
      >
        すべて ({{ totalCount }})
      </button>
      <button
        :class="{ active: todoStore.filter === 'active' }"
        @click="todoStore.setFilter('active')"
      >
        未完了 ({{ activeCount }})
      </button>
      <button
        :class="{ active: todoStore.filter === 'completed' }"
        @click="todoStore.setFilter('completed')"
      >
        完了済み ({{ completedCount }})
      </button>
    </div>
    
    <!-- Todoリスト -->
    <div v-if="filteredTodos.length" class="todo-list">
      <div class="todo-controls">
        <button @click="toggleAll">
          {{ todoStore.allCompleted ? 'すべて未完了にする' : 'すべて完了にする' }}
        </button>
        <button @click="todoStore.clearCompleted" v-if="completedCount">
          完了済みを削除
        </button>
      </div>
      
      <ul>
        <li
          v-for="todo in filteredTodos"
          :key="todo.id"
          :class="{ completed: todo.completed }"
          class="todo-item"
        >
          <input
            type="checkbox"
            :checked="todo.completed"
            @change="todoStore.toggleTodo(todo.id)"
          >
          <span class="todo-text">{{ todo.text }}</span>
          <span class="todo-date">{{ todo.createdAt.toLocaleString() }}</span>
          <button @click="todoStore.removeTodo(todo.id)" class="delete-btn">削除</button>
        </li>
      </ul>
    </div>
    
    <!-- 空の状態 -->
    <div v-else class="empty-state">
      <p>{{ todoStore.totalCount ? 'フィルター条件に一致するTodoがありません' : 'Todoがありません' }}</p>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.todo-form {
  display: flex;
  margin-bottom: 20px;
}

.todo-form input {
  flex-grow: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px 0 0 4px;
}

.todo-form button {
  padding: 8px 16px;
  border: none;
  background-color: #42b983;
  color: white;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
}

.todo-filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-filters button {
  padding: 6px 12px;
  background-color: #f2f2f2;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

.todo-filters button.active {
  background-color: #42b983;
  color: white;
  border-color: #42b983;
}

.todo-controls {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}

.todo-controls button {
  padding: 6px 12px;
  background-color: #f2f2f2;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

.todo-list ul {
  list-style-type: none;
  padding: 0;
}

.todo-item {
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-item:last-child {
  border-bottom: none;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

.todo-text {
  flex-grow: 1;
  margin: 0 10px;
}

.todo-date {
  color: #999;
  font-size: 0.8em;
  margin-right: 10px;
}

.delete-btn {
  padding: 4px 8px;
  background-color: #ff4c4c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.empty-state {
  text-align: center;
  color: #999;
  font-style: italic;
  padding: 20px;
}
</style>

9. Piniaの高度な機能

Piniaには、より高度なユースケースに対応するための機能があります。

永続化プラグイン

ページのリロード後もストアの状態を保持するために、永続化プラグインを使用できます。

// src/plugins/piniaPersist.js
import { watch } from 'vue'

// 簡単な永続化プラグイン
export function createPiniaPersistPlugin(options = {}) {
  const storage = options.storage || localStorage
  const key = options.key || 'pinia'
  
  return function({ store }) {
    // 初期状態をロード
    const savedState = JSON.parse(storage.getItem(`${key}-${store.$id}`))
    
    if (savedState) {
      store.$patch(savedState)
    }
    
    // 状態の変更を監視して保存
    watch(
      () => store.$state,
      (state) => {
        storage.setItem(`${key}-${store.$id}`, JSON.stringify(state))
      },
      { deep: true }
    )
    
    // ストアにリセットメソッドを追加
    store.$clearPersistedState = () => {
      storage.removeItem(`${key}-${store.$id}`)
    }
  }
}

// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createPiniaPersistPlugin } from './plugins/piniaPersist'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

// プラグインを使用
pinia.use(createPiniaPersistPlugin({
  storage: sessionStorage, // localStorage or sessionStorage
  key: 'my-app'
}))

app.use(pinia)
app.mount('#app')

ストア間のフック

Piniaでは、ストアの状態が変更されたときにフックを実行できます。

// src/plugins/piniaHooks.js
export function createPiniaHooksPlugin() {
  return function({ store }) {
    // 元のアクションを保存
    const originalActions = { ...store }
    
    // アクションの前後にフックを追加
    Object.keys(originalActions).forEach(actionName => {
      if (typeof originalActions[actionName] === 'function' && !actionName.startsWith(')) {
        store[actionName] = async function(...args) {
          console.log(`[${store.$id}] ${actionName} 実行前`, args)
          
          try {
            const result = await originalActions[actionName].apply(this, args)
            console.log(`[${store.$id}] ${actionName} 実行成功`, result)
            return result
          } catch (error) {
            console.error(`[${store.$id}] ${actionName} 実行エラー`, error)
            throw error
          }
        }
      }
    })
    
    // 状態変更の監視
    store.$subscribe((mutation, state) => {
      console.log(`[${store.$id}] 状態が変更されました`, mutation)
    })
  }
}

// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createPiniaHooksPlugin } from './plugins/piniaHooks'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

// フックプラグインを使用
pinia.use(createPiniaHooksPlugin())

app.use(pinia)
app.mount('#app')

TypeScriptとの統合

Piniaは、TypeScriptと非常に相性が良いです。

// src/stores/user.ts
import { defineStore } from 'pinia'

interface User {
  id: number
  name: string
  email: string
  isAdmin: boolean
}

interface UserState {
  currentUser: User | null
  isAuthenticated: boolean
  token: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    currentUser: null,
    isAuthenticated: false,
    token: null
  }),
  
  getters: {
    fullName(): string {
      return this.currentUser ? this.currentUser.name : 'ゲスト'
    },
    
    isAdmin(): boolean {
      return !!this.currentUser?.isAdmin
    }
  },
  
  actions: {
    async login(email: string, password: string): Promise {
      try {
        // ログイン処理...
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ email, password })
        })
        
        if (!response.ok) {
          throw new Error('認証に失敗しました')
        }
        
        const data = await response.json()
        
        this.currentUser = data.user
        this.isAuthenticated = true
        this.token = data.token
        
        return true
      } catch (error) {
        console.error(error)
        return false
      }
    },
    
    logout(): void {
      this.currentUser = null
      this.isAuthenticated = false
      this.token = null
    }
  }
})

10. Piniaと状態管理のベストプラクティス

Piniaを効果的に使用するためのベストプラクティスを紹介します。

効率的なストア構造の例

// src/stores/index.js
// すべてのストアをエクスポート
export * from './user'
export * from './product'
export * from './cart'
export * from './notification'

// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useNotificationStore } from './notification'

// Composition API構文
export const useUserStore = defineStore('user', () => {
  // 通知ストアをインポート
  const notificationStore = useNotificationStore()
  
  // 状態
  const currentUser = ref(null)
  const isLoading = ref(false)
  const error = ref(null)
  
  // ゲッター
  const isAuthenticated = computed(() => !!currentUser.value)
  const isAdmin = computed(() => currentUser.value?.role === 'admin')
  
  // アクション
  async function login(credentials) {
    isLoading.value = true
    error.value = null
    
    try {
      // ログイン処理...
      const user = await api.login(credentials)
      currentUser.value = user
      
      // 通知を表示
      notificationStore.show({
        type: 'success',
        message: 'ログインしました'
      })
      
      return true
    } catch (e) {
      error.value = e.message
      
      notificationStore.show({
        type: 'error',
        message: `ログインに失敗しました: ${e.message}`
      })
      
      return false
    } finally {
      isLoading.value = false
    }
  }
  
  function logout() {
    currentUser.value = null
    // その他のログアウト処理...
    
    notificationStore.show({
      type: 'info',
      message: 'ログアウトしました'
    })
  }
  
  // 公開するプロパティとメソッド
  return {
    currentUser,
    isLoading,
    error,
    isAuthenticated,
    isAdmin,
    login,
    logout
  }
})

11. まとめ

本章では、Vue.jsにおける状態管理の概念と、Piniaを使用した効果的な状態管理の方法について学びました。

状態管理の必要性とパターン、従来のVuexからPiniaへの移行理由、Piniaの基本的な使い方、ストアの設計パターン、そしてPiniaの高度な機能について理解を深めました。

Piniaは、Vue 3のComposition APIと完全に統合され、TypeScriptとの相性が良く、シンプルで直感的なAPIを提供する強力な状態管理ライブラリです。これを活用することで、大規模なVue.jsアプリケーションでも状態を効率的に管理できます。

次の章では、Vue.jsアプリケーションへのTypeScriptの導入について学びます。

目次に戻る