本文 AI 產出,尚未審核

Vue3 Pinia 狀態管理:在 Composition API 中使用 Store

簡介

在單頁應用(SPA)中,元件之間的資料共享與同步是常見且重要的需求。Vue 2 時代的 Vuex 已經提供了完整的全域狀態管理機制,但隨著 Vue 3 的推出,官方更推薦使用 Pinia 這個輕量、直覺且與 Composition API 完全相容的庫。

本篇文章將聚焦於 「Composition API 中使用 Pinia Store」,說明如何在 Vue 3 專案裡建立、使用與維護全域狀態。透過實作範例,你將了解 Pinia 為何比 Vuex 更簡潔、效能更佳,並能快速上手於真實專案中。


核心概念

1. Pinia 的基本結構

Pinia 的 Store 由三個主要部分組成:

部分 功能 在 Composition API 中的寫法
state 保存可被觀測的資料 return { count: ref(0) }
getters 由 state 派生的計算屬性 doubleCount: (state) => state.count * 2
actions 改變 state 或執行非同步邏輯 increment() { this.count++ }

重點:在 Composition API 中,state 可以直接使用 ref / reactive,而不需要 Vuex 那樣的 mutations。

2. 建立 Pinia 實例與掛載

在 main.js(或 main.ts)中,我們先建立 Pinia 實例,然後把它掛載到 Vue 應用上:

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

const app = createApp(App)

// 建立 Pinia 實例
const pinia = createPinia()
app.use(pinia)   // 掛載到 Vue 應用

app.mount('#app')

提示:若有多個 Store,僅需建立一次 Pinia,所有 Store 都會共享同一個 Pinia 實例。

3. 定義 Store:使用 defineStore

Pinia 提供 defineStore 這個函式,讓我們以 Composition API 的方式撰寫 Store。以下是一個最簡單的計數器 Store:

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

export const useCounterStore = defineStore('counter', () => {
  // ---------- state ----------
  const count = ref(0)

  // ---------- getters ----------
  const doubleCount = computed(() => count.value * 2)

  // ---------- actions ----------
  function increment(step = 1) {
    count.value += step
  }

  function decrement(step = 1) {
    count.value -= step
  }

  // 必須回傳要公開的屬性與方法
  return { count, doubleCount, increment, decrement }
})

為什麼要使用 defineStore 的第二個參數是函式?

  • 更好的 TypeScript 支援:函式內部的型別推斷更準確。
  • 符合 Composition API 思維:所有邏輯都寫在同一個函式裡,易於拆解與重用。

4. 在元件中使用 Store

<template>
  <div class="counter">
    <p>目前值:{{ counter.count }}</p>
    <p>兩倍值:{{ counter.doubleCount }}</p>
    <button @click="counter.increment()">+1</button>
    <button @click="counter.decrement()">-1</button>
  </div>
</template>

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

// 取得 Store 實例(同一個 Store 只會建立一次)
const counter = useCounterStore()
</script>

<style scoped>
.counter { text-align:center; }
button { margin: 0 5px; }
</style>

小技巧:在 script setup 中直接呼叫 useXXXStore(),即可把 Store 暴露給模板使用,省去 computed 包裝。

5. 多 Store 與模組化

在大型專案中,我們會把功能拆成多個 Store,例如 user, product, cart。以下示範一個簡易的使用者 Store,並說明如何在另一個 Store 中取得它的狀態。

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

export const useUserStore = defineStore('user', () => {
  const token = ref(localStorage.getItem('token') || '')
  const profile = ref(null)

  const isLoggedIn = computed(() => !!token.value)

  async function login(username, password) {
    // 假設調用 API,回傳 token 與使用者資料
    const response = await fakeApiLogin(username, password)
    token.value = response.token
    profile.value = response.user
    localStorage.setItem('token', token.value)
  }

  function logout() {
    token.value = ''
    profile.value = null
    localStorage.removeItem('token')
  }

  return { token, profile, isLoggedIn, login, logout }
})
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/user'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  // 直接使用其他 Store
  const userStore = useUserStore()
  const isGuest = computed(() => !userStore.isLoggedIn)

  function addItem(product, qty = 1) {
    const existed = items.value.find(i => i.id === product.id)
    if (existed) existed.qty += qty
    else items.value.push({ ...product, qty })
  }

  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
  )

  return { items, addItem, total, isGuest }
})

注意:在 Store 內部直接呼叫其他 Store(如 useUserStore())是安全的,Pinia 會在第一次使用時自動建立實例,之後皆共用同一個。

6. 持久化(Persistence)

Pinia 本身不提供持久化功能,但我們可以藉由插件或自行在 store 中加入 watch 來同步到 localStorage、sessionStorage 或 IndexedDB。

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

export function piniaPersist(pinia) {
  pinia.state.value = JSON.parse(localStorage.getItem('pinia-state') || '{}')

  // 每次 state 改變時寫回 localStorage
  pinia.state.value && watch(
    () => pinia.state.value,
    (state) => {
      localStorage.setItem('pinia-state', JSON.stringify(state))
    },
    { deep: true }
  )
}

在 main.js 中註冊:

import { piniaPersist } from '@/plugins/piniaPersist'

app.use(pinia)
pinia.use(piniaPersist)   // 套用插件

常見陷阱與最佳實踐

陷阱 說明 解決方式
直接改寫 state 在 Composition API 中,ref / reactive 的值必須透過 .value 或解構賦值修改。若忘記 .value 會造成 無法觸發更新。 使用 increment()、setX() 等 action 包裝變更,避免直接在模板中改寫。
Store 被多次建立 在非 setup 之外(如普通的 JS 檔)多次呼叫 useStore() 可能產生不同實例。 確保所有 Store 呼叫都在同一個 Pinia 實例下,且在 Vue 組件或 setup 中使用。
Getter 失去反應性 若在 defineStore 中使用普通函式返回計算結果,而非 computed,則不具備響應式。 必須使用 computed(() => ...) 包裝所有衍生值。
跨 Store 直接存取 state 直接讀寫其他 Store 的 state 會破壞 單向資料流,不易追蹤。 透過 actions 互相呼叫,或使用 getters 取得需要的資料。
持久化時的 JSON 循環參考 若 state 中包含循環參考,JSON.stringify 會拋錯。 避免把完整的 Vue 實例或函式存入 state,只保存純資料結構。

最佳實踐

  1. 保持 Store 輕量:只放置與該功能相關的 state、getter、action。
  2. 使用 TypeScript:Pinia 天生支援型別推斷,能減少錯誤。
  3. 將非同步邏輯放在 actions,保持 state 只負責資料。
  4. 模組化:每個功能領域一個 Store,避免單一 Store 變得龐大。
  5. 測試:利用 Jest 或 Vitest 撰寫 Store 的單元測試,確保 business logic 正確。

實際應用場景

場景 為何使用 Pinia 實作要點
使用者登入與權限管理 需要在多個頁面共享 token、使用者資料,且要支援持久化。 建立 userStore,在 login 時寫入 localStorage,在 router.beforeEach 中檢查 isLoggedIn。
購物車 商品列表與結算頁面都需要即時同步的購物車資料。 cartStore 中使用 ref([]) 保存 items,提供 addItem、removeItem、checkout 等 actions。
即時聊天 訊息需要在多個聊天室元件間共用,同時要支援 WebSocket 推送。 建立 chatStore,使用 socket.io 事件在 actions 中更新 messages,利用 computed 產生未讀計數。
主題切換(Dark/Light) 全站樣式需要根據使用者選擇即時變更。 themeStore 保存 mode,在 watchEffect 中動態切換 document.body.classList。
表單暫存 使用者在長表單填寫時,若不小心刷新頁面希望保留已填寫的內容。 在 formStore 中使用 watch 把 formData 同步到 sessionStorage,離開頁面時自動恢復。

總結

Pinia 為 Vue 3 帶來了 更直觀、輕量且與 Composition API 完全相容 的全域狀態管理方案。透過 defineStore 的函式寫法,我們可以:

  • 直接使用 ref / reactive 定義 state,保持與 Vue 3 的一致性。
  • 使用 computed 建立 getter,讓衍生資料自動保持響應式。
  • 把所有變更邏輯集中於 action,支援同步與非同步操作,提升可測試性。

在實務開發中,將功能切割成多個專屬 Store、善用持久化插件、遵守單向資料流原則,能讓大型專案的維護成本大幅降低。希望本篇文章能幫助你快速上手 Pinia,並在 Vue3 專案中建立穩定、可擴充的狀態管理架構。祝開發順利! 🚀