☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
vue 中級

🍍 Pinia 狀態管理:跨元件資料共享

📌 為什麼需要狀態管理?

當你的應用越來越大,元件之間需要共享資料時:

問題場景:
  Header 需要知道「使用者是否登入」
  Sidebar 需要知道「購物車有幾件商品」
  ProductPage 需要修改「購物車內容」
  CartPage 需要顯示「購物車所有商品」

如果只靠 props / emit,傳遞鏈會非常複雜:
  App → Header (props)
  App → Sidebar (props)
  App → ProductPage → AddButton (emit 好幾層)

狀態管理 = 把共用資料放到一個「全域倉庫」(store),任何元件都能直接存取。

Pinia 底層也是 JavaScript!它用的是 Vue 的響應式系統。


📌 Pinia vs Vuex

比較 Pinia (推薦) Vuex 4
API 風格 Composition API 風格 mutations 繁瑣
TypeScript 完美支援 支援但麻煩
DevTools 支援 支援
模組化 天生模組化 需要 modules
學習曲線 簡單 較複雜
檔案大小 ~1KB ~10KB

Pinia 是 Vue 官方推薦的狀態管理工具,已取代 Vuex。


📌 安裝與設定

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

const app = createApp(App)
app.use(createPinia()) // 安裝 Pinia
app.mount('#app')

📌 定義 Store

基本結構

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

// defineStore 的第一個參數是 store 的唯一 ID
export const useCounterStore = defineStore('counter', () => {
  // state — 用 ref() 定義狀態
  const count = ref(0)
  const name = ref('計數器')

  // getters — 用 computed() 定義衍生狀態
  const doubleCount = computed(() => count.value * 2)
  const isPositive = computed(() => count.value > 0)

  // actions — 用普通函式定義操作
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = 0
  }

  // 非同步 action
  async function fetchCount() {
    const response = await fetch('/api/count')
    const data = await response.json()
    count.value = data.count
  }

  // 必須 return 所有要公開的狀態和方法
  return {
    count, name,
    doubleCount, isPositive,
    increment, decrement, reset, fetchCount
  }
})

📌 在元件中使用 Store

<template>
  <div>
    <h2>{{ counterStore.name }}</h2>
    <p>數量:{{ counterStore.count }}</p>
    <p>雙倍:{{ counterStore.doubleCount }}</p>

    <button @click="counterStore.increment()">+1</button>
    <button @click="counterStore.decrement()">-1</button>
    <button @click="counterStore.reset()">歸零</button>
  </div>
</template>

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

// 直接呼叫就能取得 store 實例
const counterStore = useCounterStore()

// ⚠️ 不能解構!會失去響應性
// ❌ const { count } = counterStore
// ✅ 用 storeToRefs 解構
import { storeToRefs } from 'pinia'
const { count, doubleCount } = storeToRefs(counterStore)
// actions 可以直接解構(函式不需要響應性)
const { increment, decrement } = counterStore
</script>

📌 完整範例:購物車狀態管理

定義購物車 Store

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

export const useCartStore = defineStore('cart', () => {
  // 購物車商品列表
  const items = ref([])

  // 商品總數
  const totalItems = computed(() => {
    return items.value.reduce((sum, item) => sum + item.quantity, 0)
  })

  // 總金額
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
  })

  // 加入購物車
  function addItem(product) {
    const existing = items.value.find(item => item.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  // 移除商品
  function removeItem(productId) {
    items.value = items.value.filter(item => item.id !== productId)
  }

  // 更新數量
  function updateQuantity(productId, quantity) {
    const item = items.value.find(item => item.id === productId)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) removeItem(productId)
    }
  }

  // 清空購物車
  function clearCart() {
    items.value = []
  }

  return {
    items, totalItems, totalPrice,
    addItem, removeItem, updateQuantity, clearCart
  }
})

商品頁面使用

<!-- views/Products.vue -->
<template>
  <div>
    <h1>商品列表</h1>
    <div class="product-grid">
      <div v-for="product in products" :key="product.id" class="product-card">
        <h3>{{ product.name }}</h3>
        <p>${{ product.price }}</p>
        <button @click="cartStore.addItem(product)">
          加入購物車 🛒
        </button>
      </div>
    </div>

    <!-- 購物車小圖示 -->
    <div class="cart-badge">
      🛒 {{ cartStore.totalItems }} 件
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()

const products = ref([
  { id: 1, name: 'Vue 教學書', price: 580 },
  { id: 2, name: 'JavaScript 大全', price: 750 },
  { id: 3, name: 'TypeScript 入門', price: 420 },
  { id: 4, name: 'CSS 設計模式', price: 350 }
])
</script>

購物車頁面

<!-- views/Cart.vue -->
<template>
  <div>
    <h1>購物車</h1>

    <div v-if="cartStore.items.length === 0">
      <p>購物車是空的 😢</p>
    </div>

    <div v-else>
      <div v-for="item in cartStore.items" :key="item.id" class="cart-item">
        <span>{{ item.name }}</span>
        <span>${{ item.price }}</span>
        <div>
          <button @click="cartStore.updateQuantity(item.id, item.quantity - 1)">-</button>
          <span>{{ item.quantity }}</span>
          <button @click="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
        </div>
        <span>小計:${{ item.price * item.quantity }}</span>
        <button @click="cartStore.removeItem(item.id)">🗑️</button>
      </div>

      <hr />
      <p>總計:${{ cartStore.totalPrice }}</p>
      <button @click="cartStore.clearCart()">清空購物車</button>
    </div>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'
const cartStore = useCartStore()
</script>

📌 持久化存儲

購物車資料重新整理後會消失,用 localStorage 持久化:

// stores/cart.js(加入持久化)
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 從 localStorage 初始化
  const saved = localStorage.getItem('cart-items')
  const items = ref(saved ? JSON.parse(saved) : [])

  // 監聽變化,自動存到 localStorage
  // watch 是 Vue 的 API,底層用的是 JS 的 Proxy
  watch(items, (newItems) => {
    localStorage.setItem('cart-items', JSON.stringify(newItems))
  }, { deep: true })

  // ... 其他 getters 和 actions 同上

  return { items /* ... */ }
})

注意: localStorage瀏覽器原生 API,不是 Vue 的東西。 watch 則是 Vue 的響應式 API。分清楚哪些是框架、哪些是原生,很重要!


💡 小提醒

  • 不是所有狀態都需要放 Store——只有「跨元件共享」的資料才需要
  • 局部狀態(只有一個元件用)就用 ref() / reactive()
  • Store 是單例的——整個 App 只有一個 counter store 實例
  • Pinia 支援 Vue DevTools,可以在瀏覽器中查看/修改 Store 狀態

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊