Pinia の基本 API 解説:defineStore・state・getters・actions

Vue.js アプリケーション開発において、コンポーネント間でのデータ共有は避けて通れない課題です。小規模なアプリケーションではコンポーネント間の親子関係で解決できますが、規模が大きくなるにつれて状態管理の複雑さが増していきます。
Pinia は Vue.js の公式状態管理ライブラリとして、シンプルで直感的な API を提供します。この記事では、Pinia の核となる 4 つの基本 API(defineStore、state、getters、actions)について、実際の Todo アプリケーションを通じて段階的に学んでいきます。
背景:Vue.js における状態管理の必要性
現代の Web アプリケーション開発では、ユーザーインターフェースの複雑化に伴い、複数のコンポーネント間でデータを共有する必要性が高まっています。
mermaidflowchart TD
user[ユーザー] -->|操作| component1[ヘッダーコンポーネント]
user -->|操作| component2[サイドバーコンポーネント]
user -->|操作| component3[メインコンポーネント]
component1 -->|データ共有| shared[共有状態]
component2 -->|データ共有| shared
component3 -->|データ共有| shared
shared -->|更新通知| component1
shared -->|更新通知| component2
shared -->|更新通知| component3
上図は、複数のコンポーネントが共通の状態を参照・更新する典型的なパターンを示しています。各コンポーネントが独立して状態を更新し、その変更が他のコンポーネントに即座に反映される必要があります。
複雑な状態管理が必要な場面
Vue.js アプリケーションで状態管理が特に重要になるのは以下のようなケースです:
# | 場面 | 具体例 | 課題 |
---|---|---|---|
1 | ユーザー認証情報 | ログイン状態、ユーザープロフィール | 全画面で参照が必要 |
2 | ショッピングカート | 商品数、合計金額 | 複数画面での同期が必要 |
3 | 通知システム | エラーメッセージ、成功通知 | 任意のタイミングでの表示制御 |
4 | リアルタイム更新 | チャット、ライブデータ | 複数コンポーネントでの即時反映 |
Vue.js の標準的なデータ共有手法
Vue.js では、親子コンポーネント間でのデータ共有に props と emit を使用します:
javascript// 親コンポーネント
<template>
<ChildComponent :message="parentMessage" @update="handleUpdate" />
</template>
<script>
export default {
data() {
return {
parentMessage: 'Hello from parent'
}
},
methods: {
handleUpdate(newValue) {
this.parentMessage = newValue
}
}
}
</script>
javascript// 子コンポーネント
<template>
<div>
<p>{{ message }}</p>
<button @click="updateMessage">Update</button>
</div>
</template>
<script>
export default {
props: ['message'],
emits: ['update'],
methods: {
updateMessage() {
this.$emit('update', 'Updated message')
}
}
}
</script>
このアプローチは単純な親子関係では有効ですが、複雑な構造では限界があります。
課題:従来の状態管理手法の限界
従来の Vue.js における状態管理手法には、アプリケーションの規模拡大に伴い顕在化する課題がいくつか存在します。
Prop Drilling の問題
深いコンポーネント階層において、中間コンポーネントが不要なデータを受け渡すことになる問題です:
mermaidflowchart TD
grand[祖父コンポーネント] -->|props| parent[父コンポーネント]
parent -->|props| child[子コンポーネント]
child -->|props| target[目標コンポーネント]
grand -.->|本当に必要| target
parent -.->|不要な中継| parent
child -.->|不要な中継| child
上図のように、実際にデータが必要なのは目標コンポーネントだけなのに、中間のコンポーネントが不要な props を受け取り続けることになります。
javascript// 祖父コンポーネント
<template>
<ParentComponent :user-data="userData" />
</template>
// 父コンポーネント(userDataを使わないが中継が必要)
<template>
<ChildComponent :user-data="userData" />
</template>
// 子コンポーネント(userDataを使わないが中継が必要)
<template>
<GrandChildComponent :user-data="userData" />
</template>
// 孫コンポーネント(実際にuserDataが必要)
<template>
<div>{{ userData.name }}</div>
</template>
Event Bubbling の複雑化
深い階層からのイベント伝播も同様に複雑になります:
javascript// 孫コンポーネント
methods: {
updateUser() {
this.$emit('update-user', newUserData)
}
}
// 子コンポーネント(単なる中継)
<template>
<GrandChild @update-user="$emit('update-user', $event)" />
</template>
// 父コンポーネント(単なる中継)
<template>
<Child @update-user="$emit('update-user', $event)" />
</template>
// 祖父コンポーネント(実際の処理)
<template>
<Parent @update-user="handleUpdateUser" />
</template>
コンポーネント間の密結合
兄弟コンポーネント間でのデータ共有では、親コンポーネントを経由する必要があり、関係性が複雑になります:
mermaidsequenceDiagram
participant A as コンポーネントA
participant Parent as 親コンポーネント
participant B as コンポーネントB
A->>Parent: データ更新イベント
Parent->>Parent: 状態更新
Parent->>B: 新しいpropsを送信
B->>B: 画面を再描画
主な課題の整理
# | 課題 | 問題点 | 影響 |
---|---|---|---|
1 | Prop Drilling | 不要なコンポーネントでも props を受け取る | 保守性の低下、コードの可読性悪化 |
2 | Event Bubbling | 中間コンポーネントでの不要な中継処理 | 処理の複雑化、デバッグの困難 |
3 | 密結合 | コンポーネント間の強い依存関係 | 再利用性の低下、テストの困難 |
4 | 状態の散在 | 複数箇所での状態管理 | 整合性の確保が困難 |
これらの課題を解決するために、集中化された状態管理ライブラリが必要となります。
解決策:Pinia の基本 API 概要
Pinia は Vue.js チームが開発した現代的な状態管理ライブラリです。Vuex の後継として設計され、より直感的で型安全な API を提供します。
mermaidflowchart LR
subgraph "Pinia Store"
state[State<br/>状態データ]
getters[Getters<br/>算出プロパティ]
actions[Actions<br/>状態変更ロジック]
end
component1[コンポーネントA] -->|参照・更新| state
component2[コンポーネントB] -->|参照・更新| state
component3[コンポーネントC] -->|参照・更新| state
state --> getters
actions --> state
上図に示すように、Pinia は中央集権的な状態管理を実現し、すべてのコンポーネントが同一のストアにアクセスできます。
Pinia の 4 つの基本 API
Pinia の核となる機能は 4 つの基本 API で構成されています:
# | API 名 | 役割 | 特徴 |
---|---|---|---|
1 | defineStore | ストアの定義 | TypeScript サポート、柔軟な定義方式 |
2 | state | 状態の管理 | リアクティブなデータ保持 |
3 | getters | 派生データの算出 | Vue の computed プロパティ相当 |
4 | actions | ビジネスロジック実行 | 同期・非同期処理の統一的な記述 |
defineStore の役割と基本構造
defineStore
は Pinia ストアを定義するための関数です。ユニークなストア ID と設定オブジェクトを受け取り、ストアインスタンスを返します。
javascriptimport { defineStore } from 'pinia';
// 基本的なストア定義
export const useCounterStore = defineStore('counter', {
// ストアの設定をここに記述
});
Options API 形式でのストア定義
Vue 2 の Options API に慣れている開発者向けの記述方式です:
javascriptexport const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'カウンター',
}),
getters: {
doubleCount: (state) => state.count * 2,
displayName: (state) => `${state.name}: ${state.count}`,
},
actions: {
increment() {
this.count++;
},
async fetchInitialCount() {
// 非同期処理の例
const response = await fetch('/api/initial-count');
const data = await response.json();
this.count = data.count;
},
},
});
Composition API 形式でのストア定義
Vue 3 の Composition API に対応した、より自由度の高い記述方式です:
javascriptimport { ref, computed } from 'vue';
export const useCounterStore = defineStore(
'counter',
() => {
// state の定義
const count = ref(0);
const name = ref('カウンター');
// getters の定義
const doubleCount = computed(() => count.value * 2);
const displayName = computed(
() => `${name.value}: ${count.value}`
);
// actions の定義
function increment() {
count.value++;
}
async function fetchInitialCount() {
const response = await fetch('/api/initial-count');
const data = await response.json();
count.value = data.count;
}
return {
count,
name,
doubleCount,
displayName,
increment,
fetchInitialCount,
};
}
);
state:状態の定義と管理
state
はストア内でリアクティブなデータを管理する仕組みです。Vue の data
オプションに相当し、コンポーネント間で共有される状態を定義します。
基本的な state の定義
javascriptexport const useUserStore = defineStore('user', {
state: () => ({
// プリミティブな値
id: null,
name: '',
email: '',
isLoggedIn: false,
// オブジェクト
profile: {
avatar: '',
bio: '',
preferences: {
theme: 'light',
language: 'ja',
},
},
// 配列
posts: [],
notifications: [],
}),
});
TypeScript での型安全な state 定義
型安全性を確保するため、interface を定義してから state を作成します:
javascriptinterface User {
id: number | null
name: string
email: string
isLoggedIn: boolean
}
interface UserState {
user: User
posts: Post[]
notifications: Notification[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: {
id: null,
name: '',
email: '',
isLoggedIn: false
},
posts: [],
notifications: []
})
})
getters:算出プロパティとしての活用
getters
は state に基づいて算出される派生データを定義します。Vue の computed プロパティと同様の仕組みで、依存する state が変更された場合のみ再計算されます。
基本的な getters の使用
javascriptexport const useShoppingStore = defineStore('shopping', {
state: () => ({
items: [
{ id: 1, name: '商品A', price: 1000, quantity: 2 },
{ id: 2, name: '商品B', price: 1500, quantity: 1 },
],
}),
getters: {
// 単純な算出プロパティ
totalItems: (state) => state.items.length,
// 配列の要素を使った計算
totalPrice: (state) => {
return state.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
},
// フォーマット済みのデータ
formattedTotal: (state) => {
const total = state.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
return `¥${total.toLocaleString()}`;
},
},
});
他の getters に依存する getters
javascriptgetters: {
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
},
taxAmount: (state) => {
// this を使用して他の getter にアクセス
return Math.floor(this.totalPrice * 0.1)
},
finalPrice() {
// this を使って複数の getter を組み合わせ
return this.totalPrice + this.taxAmount
}
}
引数付き getters
特定の条件でフィルタリングしたデータを取得する場合に使用します:
javascriptgetters: {
// 引数付き getter(関数を返す)
getItemById: (state) => (id) => {
return state.items.find(item => item.id === id)
},
getItemsByCategory: (state) => (category) => {
return state.items.filter(item => item.category === category)
},
getPriceRange: (state) => (min, max) => {
return state.items.filter(item => item.price >= min && item.price <= max)
}
}
actions:状態変更とビジネスロジック
actions
は state の値を変更し、ビジネスロジックを実行するためのメソッドです。Vue の methods オプションに相当し、同期・非同期の処理を統一的に記述できます。
基本的な actions の定義
javascriptexport const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: [],
}),
actions: {
// 同期的な state 変更
increment() {
this.count++;
this.history.push({
action: 'increment',
timestamp: Date.now(),
});
},
decrement() {
this.count--;
this.history.push({
action: 'decrement',
timestamp: Date.now(),
});
},
// 引数を受け取る action
incrementBy(amount) {
this.count += amount;
this.history.push({
action: 'incrementBy',
amount,
timestamp: Date.now(),
});
},
// 複雑なロジックを含む action
reset() {
const previousCount = this.count;
this.count = 0;
this.history.push({
action: 'reset',
previousCount,
timestamp: Date.now(),
});
},
},
});
非同期処理を含む actions
API 通信やタイマー処理などの非同期処理も actions で管理できます:
javascriptexport const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false,
error: null,
}),
actions: {
async fetchUser(userId) {
// ローディング状態の設定
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/users/${userId}`
);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
const userData = await response.json();
// 成功時の state 更新
this.user = userData;
} catch (error) {
// エラー時の state 更新
this.error = error.message;
console.error('ユーザー取得エラー:', error);
} finally {
// 必ずローディング状態を解除
this.loading = false;
}
},
async updateUser(userUpdates) {
this.loading = true;
try {
const response = await fetch(
`/api/users/${this.user.id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userUpdates),
}
);
if (response.ok) {
const updatedUser = await response.json();
this.user = updatedUser;
}
} catch (error) {
this.error = error.message;
throw error; // 呼び出し元でも処理できるよう再throw
} finally {
this.loading = false;
}
},
},
});
複数の actions を組み合わせる
javascriptactions: {
async login(credentials) {
try {
const user = await this.authenticate(credentials)
await this.fetchUserProfile(user.id)
await this.loadUserPreferences()
return user
} catch (error) {
this.handleLoginError(error)
throw error
}
},
async authenticate(credentials) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!response.ok) {
throw new Error('認証に失敗しました')
}
const user = await response.json()
this.user = user
return user
},
handleLoginError(error) {
this.error = error.message
this.user = null
}
}
具体例:Todo アプリで学ぶ基本 API
実際の Todo アプリケーションを通じて、Pinia の 4 つの基本 API の使い方を段階的に学んでいきます。このセクションでは、プロジェクトの構築から完成まで、実践的な実装例を示します。
プロジェクト環境構築
まず、Vue 3 と Pinia を使用したプロジェクトの環境を構築します。
プロジェクトの初期化
bash# Vue.js プロジェクトを作成
yarn create vue@latest todo-pinia-app
# プロジェクトディレクトリに移動
cd todo-pinia-app
# 依存関係のインストール
yarn install
# Pinia の追加
yarn add pinia
Pinia のセットアップ
main.js ファイルで Pinia を Vue アプリケーションに登録します:
javascript// 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');
プロジェクト構造の確認
csssrc/
├── main.js
├── App.vue
├── components/
│ ├── TodoList.vue
│ ├── TodoItem.vue
│ └── TodoForm.vue
└── stores/
└── todo.js
defineStore でストア作成
Todo アプリケーション用のストアを defineStore
を使用して作成します。
基本的なストア定義
javascript// stores/todo.js
import { defineStore } from 'pinia';
export const useTodoStore = defineStore('todo', {
// ここにストアの設定を記述していきます
});
Todo データの型定義
TypeScript を使用する場合の型定義を行います:
javascript// stores/todo.js
interface Todo {
id: number
text: string
completed: boolean
createdAt: Date
updatedAt: Date
}
interface Filter {
ALL: 'all'
ACTIVE: 'active'
COMPLETED: 'completed'
}
// フィルターの定数定義
const FILTERS = {
ALL: 'all',
ACTIVE: 'active',
COMPLETED: 'completed'
}
ストアの基本構造作成
javascriptexport const useTodoStore = defineStore('todo', {
state: () => ({
// ここで初期状態を定義
todos: [],
filter: 'all',
nextId: 1,
}),
getters: {
// 算出プロパティをここに定義
},
actions: {
// 状態変更のロジックをここに定義
},
});
state で初期データ定義
ストアで管理する状態データを定義します。Todo アプリケーションに必要な状態を整理して実装していきます。
基本的な state の実装
javascriptexport const useTodoStore = defineStore('todo', {
state: () => ({
// Todo リストのデータ
todos: [
{
id: 1,
text: 'Pinia について学ぶ',
completed: false,
createdAt: new Date('2024-01-01T10:00:00'),
updatedAt: new Date('2024-01-01T10:00:00'),
},
{
id: 2,
text: 'Vue.js アプリを作成する',
completed: true,
createdAt: new Date('2024-01-01T11:00:00'),
updatedAt: new Date('2024-01-01T12:00:00'),
},
],
// フィルター状態
currentFilter: 'all', // 'all', 'active', 'completed'
// UI 状態
loading: false,
error: null,
// ID管理用
nextId: 3,
}),
});
リアクティビティの確認
state で定義されたデータは、Vue のリアクティブシステムにより自動的に変更を検出します:
javascript// コンポーネント内での使用例
<template>
<div>
<p>Todo数: {{ todoStore.todos.length }}</p>
<p>現在のフィルター: {{ todoStore.currentFilter }}</p>
</div>
</template>
<script setup>
import { useTodoStore } from '@/stores/todo'
const todoStore = useTodoStore()
// state の値が変更されると、テンプレートも自動的に更新される
</script>
ネストされたオブジェクトの管理
複雑なデータ構造も state で管理できます:
javascriptstate: () => ({
todos: [],
// 設定データ
settings: {
autoSave: true,
theme: 'light',
sortBy: 'createdAt', // 'createdAt', 'text', 'completed'
sortOrder: 'desc', // 'asc', 'desc'
},
// 統計データ
statistics: {
totalCreated: 0,
totalCompleted: 0,
averageCompletionTime: 0,
},
});
getters で派生データ取得
state から派生するデータや、フィルタリングされたデータを getters で定義します。
基本的な getters の実装
javascriptexport const useTodoStore = defineStore('todo', {
state: () => ({
todos: [
{ id: 1, text: 'タスク1', completed: false },
{ id: 2, text: 'タスク2', completed: true },
{ id: 3, text: 'タスク3', completed: false },
],
currentFilter: 'all',
}),
getters: {
// 完了済みTodoの数
completedCount: (state) => {
return state.todos.filter((todo) => todo.completed)
.length;
},
// 未完了Todoの数
activeCount: (state) => {
return state.todos.filter((todo) => !todo.completed)
.length;
},
// 総Todo数
totalCount: (state) => state.todos.length,
// 完了率(パーセンテージ)
completionRate: (state) => {
if (state.todos.length === 0) return 0;
const completed = state.todos.filter(
(todo) => todo.completed
).length;
return Math.round(
(completed / state.todos.length) * 100
);
},
},
});
フィルタリング機能の実装
現在のフィルター設定に応じて Todo リストを絞り込む getters を実装します:
javascriptgetters: {
// フィルター適用済みのTodoリスト
filteredTodos: (state) => {
switch (state.currentFilter) {
case 'active':
return state.todos.filter(todo => !todo.completed)
case 'completed':
return state.todos.filter(todo => todo.completed)
case 'all':
default:
return state.todos
}
},
// フィルター別の件数情報
filterCounts: (state) => ({
all: state.todos.length,
active: state.todos.filter(todo => !todo.completed).length,
completed: state.todos.filter(todo => todo.completed).length
}),
// 現在のフィルターの表示名
currentFilterLabel: (state) => {
const labels = {
all: 'すべて',
active: '未完了',
completed: '完了済み'
}
return labels[state.currentFilter] || 'すべて'
}
}
ソート機能の実装
設定に基づいて Todo をソートする getters を追加します:
javascriptgetters: {
// ソート済みのTodoリスト
sortedTodos() {
const todos = [...this.filteredTodos]
const { sortBy, sortOrder } = this.settings
todos.sort((a, b) => {
let comparison = 0
switch (sortBy) {
case 'text':
comparison = a.text.localeCompare(b.text)
break
case 'createdAt':
comparison = new Date(a.createdAt) - new Date(b.createdAt)
break
case 'completed':
comparison = a.completed - b.completed
break
default:
comparison = a.id - b.id
}
return sortOrder === 'desc' ? -comparison : comparison
})
return todos
},
// 検索機能付きの getters(引数付き)
searchTodos: (state) => (query) => {
if (!query) return state.todos
const lowerQuery = query.toLowerCase()
return state.todos.filter(todo =>
todo.text.toLowerCase().includes(lowerQuery)
)
}
}
コンポーネントでの getters 使用例
javascript<template>
<div class="todo-summary">
<h3>{{ todoStore.currentFilterLabel }}のタスク</h3>
<p>{{ todoStore.filteredTodos.length }}件のタスク</p>
<p>完了率: {{ todoStore.completionRate }}%</p>
<div class="filter-tabs">
<button
v-for="(count, filter) in todoStore.filterCounts"
:key="filter"
@click="todoStore.setFilter(filter)"
:class="{ active: todoStore.currentFilter === filter }"
>
{{ getFilterLabel(filter) }} ({{ count }})
</button>
</div>
<ul>
<li
v-for="todo in todoStore.sortedTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
{{ todo.text }}
</li>
</ul>
</div>
</template>
<script setup>
import { useTodoStore } from '@/stores/todo'
const todoStore = useTodoStore()
function getFilterLabel(filter) {
const labels = { all: 'すべて', active: '未完了', completed: '完了済み' }
return labels[filter]
}
</script>
actions で CRUD 操作実装
Todo アプリケーションの基本的な CRUD(Create, Read, Update, Delete)操作を actions で実装します。
作成(Create)操作の実装
新しい Todo を追加する action を実装します:
javascriptexport const useTodoStore = defineStore('todo', {
state: () => ({
todos: [],
nextId: 1,
loading: false,
error: null,
}),
actions: {
// Todo追加
addTodo(text) {
// 入力値の検証
if (!text || text.trim().length === 0) {
this.error = 'Todo のテキストを入力してください';
return false;
}
// 新しいTodoオブジェクトの作成
const newTodo = {
id: this.nextId++,
text: text.trim(),
completed: false,
createdAt: new Date(),
updatedAt: new Date(),
};
// 配列の先頭に追加(最新のものが上に表示される)
this.todos.unshift(newTodo);
// エラーをクリア
this.error = null;
// 統計の更新
this.statistics.totalCreated++;
return true;
},
// 複数のTodoを一括追加
addMultipleTodos(todoTexts) {
const addedTodos = [];
todoTexts.forEach((text) => {
if (text && text.trim().length > 0) {
const newTodo = {
id: this.nextId++,
text: text.trim(),
completed: false,
createdAt: new Date(),
updatedAt: new Date(),
};
addedTodos.push(newTodo);
}
});
// 一度に配列に追加
this.todos.unshift(...addedTodos);
this.statistics.totalCreated += addedTodos.length;
return addedTodos.length;
},
},
});
更新(Update)操作の実装
既存の Todo の内容や状態を変更する action を実装します:
javascriptactions: {
// Todo完了状態の切り替え
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id)
if (!todo) {
this.error = `ID: ${id} の Todo が見つかりません`
return false
}
const wasCompleted = todo.completed
todo.completed = !todo.completed
todo.updatedAt = new Date()
// 統計の更新
if (!wasCompleted && todo.completed) {
this.statistics.totalCompleted++
} else if (wasCompleted && !todo.completed) {
this.statistics.totalCompleted--
}
this.error = null
return true
},
// Todoテキストの編集
editTodo(id, newText) {
if (!newText || newText.trim().length === 0) {
this.error = 'Todo のテキストを入力してください'
return false
}
const todo = this.todos.find(t => t.id === id)
if (!todo) {
this.error = `ID: ${id} の Todo が見つかりません`
return false
}
todo.text = newText.trim()
todo.updatedAt = new Date()
this.error = null
return true
},
// 全てのTodoを完了/未完了にする
toggleAllTodos(completed = true) {
let changedCount = 0
this.todos.forEach(todo => {
if (todo.completed !== completed) {
todo.completed = completed
todo.updatedAt = new Date()
changedCount++
}
})
// 統計の更新
if (completed) {
this.statistics.totalCompleted = this.todos.length
} else {
this.statistics.totalCompleted = 0
}
return changedCount
}
}
削除(Delete)操作の実装
Todo を削除する action を実装します:
javascriptactions: {
// 特定のTodoを削除
deleteTodo(id) {
const index = this.todos.findIndex(t => t.id === id)
if (index === -1) {
this.error = `ID: ${id} の Todo が見つかりません`
return false
}
const deletedTodo = this.todos[index]
this.todos.splice(index, 1)
// 統計の更新
if (deletedTodo.completed) {
this.statistics.totalCompleted--
}
this.error = null
return true
},
// 完了済みのTodoを全て削除
clearCompleted() {
const completedCount = this.todos.filter(t => t.completed).length
this.todos = this.todos.filter(todo => !todo.completed)
this.statistics.totalCompleted = 0
return completedCount
},
// 全てのTodoを削除
clearAllTodos() {
const deletedCount = this.todos.length
this.todos = []
this.statistics.totalCompleted = 0
this.nextId = 1
return deletedCount
}
}
非同期操作の実装
API との連携を想定した非同期の CRUD 操作を実装します:
javascriptactions: {
// サーバーからTodoリストを取得
async fetchTodos() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const todos = await response.json()
// サーバーから取得したデータで更新
this.todos = todos
this.nextId = Math.max(...todos.map(t => t.id)) + 1
// 統計の再計算
this.statistics.totalCreated = todos.length
this.statistics.totalCompleted = todos.filter(t => t.completed).length
} catch (error) {
this.error = `Todo取得エラー: ${error.message}`
console.error('fetchTodos error:', error)
} finally {
this.loading = false
}
},
// サーバーにTodoを保存
async saveTodo(todo) {
this.loading = true
try {
const response = await fetch('/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(todo)
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const savedTodo = await response.json()
// ローカル状態を更新
const index = this.todos.findIndex(t => t.id === todo.id)
if (index !== -1) {
this.todos[index] = savedTodo
}
return savedTodo
} catch (error) {
this.error = `Todo保存エラー: ${error.message}`
throw error
} finally {
this.loading = false
}
}
}
フィルター管理の action
UI の状態管理も actions で実装します:
javascriptactions: {
// フィルターの変更
setFilter(filter) {
const validFilters = ['all', 'active', 'completed']
if (!validFilters.includes(filter)) {
this.error = `無効なフィルター: ${filter}`
return false
}
this.currentFilter = filter
this.error = null
return true
},
// 設定の更新
updateSettings(newSettings) {
this.settings = {
...this.settings,
...newSettings
}
},
// エラーのクリア
clearError() {
this.error = null
}
}
コンポーネントでの actions 使用例
javascript<template>
<div class="todo-app">
<form @submit.prevent="handleAddTodo">
<input
v-model="newTodoText"
placeholder="新しいタスクを入力"
:disabled="todoStore.loading"
/>
<button type="submit" :disabled="todoStore.loading">
追加
</button>
</form>
<div v-if="todoStore.error" class="error">
{{ todoStore.error }}
<button @click="todoStore.clearError">×</button>
</div>
<div v-if="todoStore.loading" class="loading">
読み込み中...
</div>
<ul class="todo-list">
<li v-for="todo in todoStore.sortedTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="todoStore.toggleTodo(todo.id)"
/>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="todoStore.deleteTodo(todo.id)">
削除
</button>
</li>
</ul>
<div class="actions">
<button @click="todoStore.toggleAllTodos(true)">
全て完了
</button>
<button @click="todoStore.clearCompleted()">
完了済みを削除
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useTodoStore } from '@/stores/todo'
const todoStore = useTodoStore()
const newTodoText = ref('')
// 新しいTodoの追加
function handleAddTodo() {
if (todoStore.addTodo(newTodoText.value)) {
newTodoText.value = ''
}
}
// コンポーネントマウント時にデータを取得
onMounted(() => {
todoStore.fetchTodos()
})
</script>
この Todo アプリケーションの実装により、Pinia の 4 つの基本 API の実践的な使い方を学習できます。各 API が連携してリアクティブな状態管理を実現している様子を確認できるでしょう。
まとめ
この記事では、Pinia の 4 つの基本 API(defineStore、state、getters、actions)について、理論から実践まで段階的に学習しました。
Pinia の基本 API のポイント
API | 役割 | 特徴 | 使用場面 |
---|---|---|---|
defineStore | ストアの定義 | 型安全性、柔軟な記述方式 | ストア作成時 |
state | 状態データの管理 | リアクティブ、ネスト対応 | データ保持 |
getters | 派生データの算出 | キャッシュ機能、依存関係 | 計算処理 |
actions | ビジネスロジック実行 | 同期・非同期統一 | 状態変更 |
Vue.js 開発における Pinia のメリット
Pinia を使用することで、従来の Vue.js 開発における課題を効果的に解決できます:
解決される課題:
- Prop Drilling による煩雑なデータ受け渡し
- Event Bubbling の複雑な中継処理
- コンポーネント間の密結合
- 状態管理の分散による整合性の問題
得られる効果:
- 中央集権的な状態管理によるコードの整理
- TypeScript との優れた連携による型安全性
- 直感的な API による学習コストの軽減
- Vue DevTools との統合によるデバッグ支援
実装時の重要なポイント
Todo アプリケーションの実装を通じて学んだ重要なポイントを整理します:
state 設計時:
- 初期値を適切に設定する
- ネストしたオブジェクトも適切に管理される
- TypeScript との組み合わせで型安全性を確保
getters 活用時:
- 計算処理は getters で実装し、キャッシュ機能を活用する
- 他の getters への依存関係も明確に管理できる
- 引数付き getters でフィルタリング機能を実現
actions 実装時:
- 同期・非同期処理を統一的に記述できる
- エラーハンドリングとローディング状態の管理を忘れずに
- 複数の actions を組み合わせて複雑な処理を実現
次のステップ
Pinia の基本 API を習得した後は、以下の発展的なトピックに取り組むことをお勧めします:
- プラグインシステム:カスタムプラグインの作成と活用
- パフォーマンス最適化:大規模アプリケーションでの最適化手法
- テスト戦略:Pinia を使用したアプリケーションのテスト方法
- SSR/SSG 対応:Nuxt.js との連携による Server-Side Rendering
Pinia は Vue.js エコシステムの中核を担う状態管理ライブラリとして、今後も継続的に発展していくでしょう。基本 API をしっかりと理解することで、より効率的で保守しやすい Vue.js アプリケーションの開発が可能になります。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来