Pinia — グローバル状態管理
この章では、Pinia を使ってコンポーネントやページをまたぐ共有データを管理し、「記事のお気に入り」機能を実装します。
なぜグローバル状態管理が必要なのか?
これまで、コンポーネント間のデータは props と emit で渡してきました。
これは親子関係が単純なケースでは十分ですが、次のような状況では対応が難しくなります:
- お気に入り状態をホームページと詳細ページで同期する必要がある
- ユーザーのログイン状態をすべてのページで共有する必要がある
- カートのデータをナビバー、商品ページ、決済ページの間で受け渡す必要がある
props の階層ごとの受け渡しの課題:A コンポーネントのデータを D コンポーネントに渡すには、B、C の2層を経由する必要があり、中間のコンポーネント自身はこのデータを必要としません。
PiniaPinia は Vue3 公式推奨の状態管理ライブラリで、グローバルな「データセンター」を提供します。どのコンポーネントからでも直接読み書きでき、階層ごとの受け渡しは不要です。
Pinia のインストールと登録
プロジェクト作成時に Pinia を選択した場合は、インストール手順をスキップしてください。
それ以外の場合は:
$ npm install pinia
main.js で登録:
例
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// Pinia インスタンスを作成して登録
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
defineStore — ストアの定義
Pinia の中核はStore(ストア)であり、各 Store が独立した状態を管理します。
ストアには次の3つの部分があります:
| 部分 | 役割 | 対応 |
|---|---|---|
| state | データの保存 | コンポーネントの data / ref |
| getters | 派生計算データ | コンポーネントの computed |
| actions | データを変更するメソッド | コンポーネントの methods / function |
例
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// defineStore('ストア名', () => { ... })
// 第2引数は関数形式で、Composition API のスタイルに準拠
export const useFavoriteStore = defineStore('favorite', () => {
// ====== state:ref で状態を定義 ======
// お気に入りの記事 ID を配列で保存する
const favoriteIds = ref([])
// localStorage からお気に入りリストを復元
const saved = localStorage.getItem('blog-favorites')
if (saved) {
favoriteIds.value = JSON.parse(saved)
}
// ====== getters:computed で算出プロパティを定義 ======
const favoriteCount = computed(() => favoriteIds.value.length)
// 指定した記事がお気に入り済みかどうかを判定
function isFavorite(articleId) {
return favoriteIds.value.includes(articleId)
}
// ====== actions:通常の関数で変更メソッドを定義 ======
// お気に入り状態を切り替える(お気に入り → 解除、未お気に入り → お気に入り)
function toggleFavorite(articleId) {
const index = favoriteIds.value.indexOf(articleId)
if (index === -1) {
favoriteIds.value.push(articleId) // お気に入りに追加
} else {
favoriteIds.value.splice(index, 1) // お気に入りを解除
}
// localStorage に同期(永続化)
localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds.value))
}
return { favoriteIds, favoriteCount, isFavorite, toggleFavorite }
})
defineStore の第2引数には2つの書き方があります:Options Store(Vue2 の data/methods/computed のオブジェクト形式に類似した書き方)とSetup Store(Composition API を使用する関数形式)です。Setup Store の使用を推奨します。コンポーネント内の
<script setup>書き方と完全に一致するため、学習コストが最も低くなります。
コンポーネントで Store を使用する
どのコンポーネントでも、useFavoriteStore()ストアインスタンスを取得し、state を直接読み書きして action を呼び出すことができます。
BlogCard にお気に入りボタンを追加する
例
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const props = defineProps({
id: Number,
title: String,
summary: String,
date: String,
category: String
})
const favoriteStore = useFavoriteStore()
// お気に入りボタンクリック時に呼び出される
function handleFavorite(e) {
e.preventDefault() // RouterLink の遷移を防ぐ
favoriteStore.toggleFavorite(props.id)
}
</script>
<template>
<RouterLink :to="{ name: 'post', params: { id } }" class="card-link">
<div class="card">
<span class="tag">{{ category }}</span>
<h3>{{ title }}</h3>
<p>{{ summary }}</p>
<div class="card-footer">
<span class="date">{{ date }}</span>
<!-- お気に入りボタン:お気に入り済みは塗りつぶし、未お気に入りは枠線のみ表示 -->
<button class="fav-btn" @click="handleFavorite">
{{ favoriteStore.isFavorite(id) ? '♥' : '♡' }}
</button>
</div>
</div>
</RouterLink>
</template>
<style scoped>
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.fav-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #e74c3c;
padding: 4px 8px;
}
.fav-btn:hover {
transform: scale(1.2);
}
</style>
詳細ページにもお気に入りボタンを追加する
例
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const favoriteStore = useFavoriteStore()
// ... その他のコード
</script>
<template>
<article v-if="article">
<div class="post-header">
<h1>{{ article.title }}</h1>
<!-- 詳細ページのお気に入りボタン:状態はホームページと完全に同期 -->
<button class="fav-btn" @click="favoriteStore.toggleFavorite(article.id)">
{{ favoriteStore.isFavorite(article.id) ? '♥お気に入り済み' : '♡ お気に入り' }}
</button>
</div>
<!-- ... -->
</article>
</template>
ホームページと詳細ページのお気に入り状態は完全に同期します:ホームページで記事をお気に入りに追加すると、詳細ページで自動的に「お気に入り済み」と表示されます。
これがグローバル状態管理の価値です:データは1つだけ保存され、すべてのコンポーネントが同じソースを読み書きします。emit での受け渡しも、props の階層ごとの転送も不要です。
NavBar にお気に入り数を表示する
ナビバーの右側にお気に入り数を表示し、クリックするとお気に入り済みの記事のリストを表示できます。
例
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const favoriteStore = useFavoriteStore()
</script>
<template>
<header class="navbar">
<a href="/" class="logo">サンプル Blog</a>
<nav>
<!-- お気に入り数のバッジ -->
<span class="fav-badge" v-if="favoriteStore.favoriteCount > 0">
お気に入り {{ favoriteStore.favoriteCount }}
</span>
</nav>
</header>
</template>
<style scoped>
.fav-badge {
background: #e74c3c;
color: #fff;
padding: 4px 10px;
border-radius: 12px;
font-size: 13px;
}
</style>
NavBar の数は最新のお気に入りリストと自動的に同期します——記事をお気に入りに追加または解除すると、ナビバーの数字はすぐに変わります。
Pinia の3大中核機能のまとめ
| 機能 | 対応コード | お気に入り機能での具体例 |
|---|---|---|
| グローバル共有 | 任意のコンポーネントで useXxxStore() が使用可能 | NavBar、BlogCard、PostView が同じお気に入りリストを共有する |
| リアクティブ | state が変化すると、すべての利用側が自動的に更新 | お気に入りに追加すると、ホームページのボタンが赤くなり、ナビバーの数が +1、詳細ページも同期して更新される |
| 永続化 | actions で localStorage に書き込み | ページを更新してもお気に入りリストは残る |
本章のまとめ
この章では、Pinia によるグローバル状態管理の完全な使い方を学びました:defineStore によるストアの定義(state/getters/actions)、コンポーネントでの useXxxStore によるインスタンス取得、そして localStorage によるデータの永続化の実装です。
「記事のお気に入り」機能により、ホームページ、詳細ページ、ナビバー間のデータ同期が非常に自然になり、すべてのコンポーネントが自動的に一貫性を保ちます。
その他の拡張