Pinia入門チュートリアル

Vue3では、状態管理は避けて通れない話題です。

Pinia は Vue.js の軽量な状態管理ライブラリで、コンポーネント間で状態を共有・管理できるようにします。Pinia はグローバルなデータ倉庫と考えることができ、すべてのコンポーネントがここからデータを取得したり更新したりできます。

この章では、Vue3 公式が推奨する状態管理ライブラリ —— Pinia を紹介します。Vuex と比較して、Pinia はよりシンプルで、Vue3 の Composition API の考え方に合った状態管理ソリューションを提供します。

次の図は、両者の構造の違いを比較したものです。

  • Vuexは単一ストアに多階層モジュールを加えたツリー構造を採用しており、階層が固定され、mutationsに依存するため、全体的に重くなっています。
  • Piniaは複数の独立したStoreで構成され、フラットで軽量、モジュール分けがなく、ネームスペースの負担もありません。

Piniaの主な特徴:

  • Vue2とVue3をサポート
  • シンプルなAPI設計
  • 完全なTypeScriptサポート
  • コンポジションAPIをサポート
  • モジュール化設計、モジュールのネストは不要

Piniaのフラットな構造により、コンポーネントは任意のStoreに直接接続でき、必要なものを自由に取得できます。ツリー状のモジュールやネームスペースのパスを経由しないため、状態の流れはシンプルかつ直接的で、結合度が低くなります。


インストールと設定

Viteでプロジェクトを作成:

npm create vite@latest vue-pinia-demo --template vue
cd vue-pinia-demo
npm install

Piniaのインストール

まず、Vue3プロジェクトにPiniaをインストールします:

npm install pinia
# 或者
yarn add pinia

Piniaには3つの部分があります:

  • state:データを格納
  • getters:派生データを計算
  • actions:ロジックを実行し、stateを変更

配置 Pinia

src/main.js または src/main.ts で Pinia を設定します:

实例

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

// 创建 Pinia 实例
const pinia = createPinia()
// 创建 Vue 应用
const app = createApp(App)

// 使用 Pinia
app.use(pinia)
app.mount('#app')

最初のStoreを作成

StoreはPiniaにおけるデータの保管庫です。簡単なカウンターStoreを作成してみましょう。

定义 Store

新建 src/stores/useCounter.js:

实例

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

// 使用 defineStore 定义 store
// 最初の引数は store の一意の ID です
// 2 番目のパラメータは store の設定オプションです
export const useCounterStore = defineStore('counter', {
  // state: store の状態データを定義する
  state: () => ({
    count: 0,
    name: 'マイカウンター'
  }),

  // getters: state に基づく算出プロパティを定義します
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用 this 访问其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },

  // actions: state を変更するメソッドを定義します
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    // 可以接收参数
    incrementBy(amount) {
      this.count += amount
    },
    // 异步 action
    async incrementAsync() {
      // 模拟异步操作
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

Storeの構造の説明

表を使ってStoreの各部分を理解しましょう:

部分 作用 示例
state 保存するデータの定義 count: 0
getters stateに基づく算出プロパティ doubleCount: state => state.count * 2
actions stateを変更するメソッド increment() { this.count++ }

コンポーネントでStoreを使用

基本使用

src/components/CounterComponent.vue 组件代码如下:

实例

<!-- CounterComponent.vue -->
<template>
  <div class="counter">
    <h3>{{ store.name }}</h3>
    <p>当前计数: {{ store.count }}</p>
    <p>双倍计数: {{ store.doubleCount }}</p>
    <p>双倍加一: {{ store.doubleCountPlusOne }}</p>
   
    <button @click="store.increment()">+1</button>
    <button @click="store.decrement()">-1</button>
    <button @click="store.incrementBy(5)">+5</button>
    <button @click="store.incrementAsync()">异步 +1</button>
   
    <button @click="reset">重置</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../stores/useCounter'

// setup で store を使用する
const store = useCounterStore()

// 状態をリセットする方法
function reset() {
store.$reset() // $reset 方法可以重置 state 到初始值
}
</script>

src/App.vueを以下のように変更:

实例

<script setup>
import CounterComponent from './components/CounterComponent.vue'
</script>

<template>
  <CounterComponent />
</template>

プロジェクト全体の構造:

执行npm run devコマンドを実行し、ブラウザでアクセスします。http://localhost:5173/、動作を確認:

响应式解构

テンプレート内でstateのプロパティを直接使用したい場合は、次を使用できます。storeToRefs:

实例

<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>名称: {{ name }}</p>
  </div>
</template>

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

const store = useCounterStore()

// 使用 storeToRefs 保持响应式
const { count, name } = storeToRefs(store)

// 注意:直接解构会失去响应式!
// 错误写法:const { count, name } = store
</script>

コンポジションAPIスタイルのStore

PiniaはコンポジションAPIスタイルでStoreを定義することもサポートしています:

实例

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

export const useUserStore = defineStore('user', () => {
  // state
  const user = ref(null)
  const isLoggedIn = ref(false)

  // getters
  const userName = computed(() => user.value?.name || '游客')
  const userAge = computed(() => user.value?.age || 0)

  // actions
  function login(userData) {
    user.value = userData
    isLoggedIn.value = true
  }

  function logout() {
    user.value = null
    isLoggedIn.value = false
  }

  return {
    user,
    isLoggedIn,
    userName,
    userAge,
    login,
    logout
  }
})

Store同士の連携

複数のStoreは相互に呼び出すことができます:

实例

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),

  actions: {
    addItem(product) {
      this.items.push(product)
     
      // 他の store の action を呼び出す
      const userStore = useUserStore()
      if (userStore.isLoggedIn) {
        // 同步到用户购物车
        this.syncToUserCart()
      }
    }
  }
})

実践的な応用例:ショッピングカート

完全なショッピングカートの例を作成しましょう:

实例

// stores/products.js
export const useProductStore = defineStore('products', {
  state: () => ({
    products: [
      { id: 1, name: '笔记本电脑', price: 5999, stock: 10 },
      { id: 2, name: '智能手机', price: 3999, stock: 20 },
      { id: 3, name: '无线耳机', price: 299, stock: 50 }
    ]
  }),

  getters: {
    // 根据 ID 获取商品
    getProductById: (state) => (id) => {
      return state.products.find(product => product.id === id)
    },
    // に庫のある商品を取得
    availableProducts: (state) => {
      return state.products.filter(product => product.stock > 0)
    }
  }
})

实例

// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // { productId, quantity }
    discount: 0
  }),

  getters: {
    // 购物车商品总数
    totalItems: (state) => {
      return state.items.reduce((total, item) => total + item.quantity, 0)
    },
    // 购物车总金额
    totalPrice: (state) => {
      const productStore = useProductStore()
      return state.items.reduce((total, item) => {
        const product = productStore.getProductById(item.productId)
        return total + (product?.price || 0) * item.quantity
      }, 0)
    },
    // 折后总价
    finalPrice: (state) => {
      return state.totalPrice * (1 - state.discount / 100)
    }
  },

  actions: {
    // 添加商品到购物车
    addToCart(productId, quantity = 1) {
      const existingItem = this.items.find(item => item.productId === productId)
     
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({ productId, quantity })
      }
    },

    // カートから商品を削除
    removeFromCart(productId) {
      this.items = this.items.filter(item => item.productId !== productId)
    },

    // 清空购物车
    clearCart() {
      this.items = []
      this.discount = 0
    },

    // 设置折扣
    setDiscount(percent) {
      this.discount = Math.max(0, Math.min(100, percent))
    }
  }
})

コンポーネントでショッピングカートを使用

实例

<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h3>购物车 ({{ cart.totalItems }} 件商品)</h3>
   
    <div v-if="cart.items.length === 0">
      <p>カートが空です</p>
    </div>
   
    <div v-else>
      <div v-for="item in cartItems" :key="item.product.id" class="cart-item">
        <span>{{ item.product.name }}</span>
        <span>¥{{ item.product.price }}</span>
        <span>数量: {{ item.quantity }}</span>
        <span>小计: ¥{{ item.product.price * item.quantity }}</span>
        <button @click="cart.removeFromCart(item.product.id)">删除</button>
      </div>
     
      <div class="cart-summary">
        <p>总价: ¥{{ cart.totalPrice }}</p>
        <p v-if="cart.discount >0">折扣: {{ cart.discount }}%</p>
        <p>实付: ¥{{ cart.finalPrice }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'

const cart = useCartStore()
const products = useProductStore()

// 计算购物车商品详情
const cartItems = computed(() => {
  return cart.items.map(item => ({
    product: products.getProductById(item.productId),
    quantity: item.quantity
})).filter(item => item.product) // 存在しない商品をフィルタリングする
})
</script>

ベストプラクティスと注意事項

1. Storeの命名規約

实例

// 良い命名
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })

// 避けるべき命名
export const userStore = defineStore('user', { /* ... */ }) // 缺少 use 前缀

2. 状態の初期化

实例

// 推奨:関数を使用して初期状態を返す
state: () => ({
  items: [],
  loading: false,
  error: null
})

// 非推奨:オブジェクトを直接使用する
state: {
  items: []  // これにより、すべてのインスタンスが同じ配列を共有することになります!
}

3. 非同期操作の処理

实例

actions: {
  async fetchUserData(userId) {
    this.loading = true
    this.error = null
   
    try {
      const response = await api.getUser(userId)
      this.user = response.data
    } catch (error) {
      this.error = error.message
    } finally {
      this.loading = false
    }
  }
}

4. データの永続化

永続化が必要なデータには、プラグインを使用できます:

npm install pinia-plugin-persistedstate

实例

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {}
  }),
 
  // 永続化を有効にする
  persist: true
})

Pinia API

カテゴリ API 説明
作成 defineStore 独立した store を定義し、state / getters / actions を含める
インスタンス化 createPinia Pinia ルートインスタンスを作成し、app.use() に使用する
コンポーネントでの使用 useStore()(ユーザー定義、例:useUserStore) 特定の store を呼び出し、リアクティブな state、getter、action を返す
状態 state 可変なグローバル状態を定義するためのオブジェクト関数を返す
状態操作 store.$state state全体を直接読み書きする(オブジェクトレベル)
状態操作 store.$patch() state を一括変更し、オブジェクトと関数の2つのモードをサポート
状態の置き換え store.$reset() 初期 state にリセットします。setup 方式で定義した場合のみ使用可能
状態の購読 store.$subscribe() state の変化を監視し、ローカル永続化に適している
Action の呼び出し store.$onAction() action の呼び出し前後を監視し、ログやトラッキングに利用できる
Getter getters 派生データ。state に基づいて自動的にキャッシュされる
プラグイン pinia.use() プラグインを登録して、store の機能を拡張する
定義方法 defineStore(id, options) Options API での記述方法
定義方法 defineStore(id, () => {...}) Setup での記述方法。state / getter / action を返す
Store のプロパティ store.$id 現在の store の一意の識別子
Store のプロパティ store.$ready(一部のバージョン) Store の初期化完了後の状態フラグ
永続化(プラグイン) persist ストレージプラグインを有効にする(localStorage など)
ユーティリティ関数 storeToRefs() state / getter を refs に変換し、リアクティブ性を維持する

詳細は Pinia 公式サイトを参照:https://pinia.vuejs.org/

その他の拡張