本文 AI 產出,尚未審核

Vue3 Pinia 狀態管理:stategettersactions 完整指南


簡介

在單頁應用(SPA)中,資料的共享與同步 是開發者常面臨的挑戰。Vue3 原生提供了 provide/injectpropsemit 等機制,但當應用規模擴大、元件層級變深時,這些方式會變得笨拙且難以維護。

Pinia 作為 Vue 官方推薦的 狀態管理庫,以「輕量、直覺、型別安全」為設計核心,取代了過去的 Vuex。它把 狀態(state)衍生資料(getters)行為(actions) 明確分離,使得程式碼結構更清晰、測試更容易。

本單元將深入探討 Pinia 的三大核心概念,透過實作範例說明如何在 Vue3 專案中建立、使用與管理全域狀態,並提供常見陷阱與最佳實踐,幫助您在實務開發中快速上手、有效維護。


核心概念

1. state:儲存可變的資料

state 就像 Vue 元件的 data,負責保存應用的唯一真相(single source of truth)。在 Pinia 中,我們使用 defineStore 來建立一個 Store,回傳一個包含 state 的函式。

// src/stores/todoStore.js
import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todo', {
  // 1️⃣ state 必須是回傳物件的函式,確保每個實例都有自己的獨立資料
  state: () => ({
    // 目前的待辦清單
    list: [] as Array<{ id: number; text: string; done: boolean }>,
    // 用來產生唯一 id 的計數器
    nextId: 1,
  }),
})

重點state 必須是 函式,而非直接物件,這樣才能避免多個 Store 實例共享同一個引用,導致不可預期的副作用。


2. getters:計算屬性、衍生資料

getters 與 Vue 元件的 computed 類似,用來根據現有 state 計算出新值,且會自動快取(cache),只有當依賴的 state 改變時才重新計算。

// src/stores/todoStore.js(續)
export const useTodoStore = defineStore('todo', {
  state: () => ({
    list: [] as Array<{ id: number; text: string; done: boolean }>,
    nextId: 1,
  }),

  // 2️⃣ getters:可寫成普通函式或是 getter 物件
  getters: {
    // 回傳未完成的項目數量
    unfinishedCount(state) {
      return state.list.filter(item => !item.done).length
    },

    // 回傳已完成的項目陣列(使用 getter 的快取特性)
    completedItems(state) {
      return state.list.filter(item => item.done)
    },

    // 以 getter 方式取得全部項目(示範使用 this 取得其他 getter)
    totalItems(): number {
      // `this` 代表 Store 本身,可直接存取其他 getter
      return this.unfinishedCount + this.completedItems.length
    },
  },
})

技巧:在 getters 中避免副作用(例如修改 state),只做純粹的計算。若需要更複雜的邏輯,考慮搬到 actions 裡。


3. actions:變更 state 或執行非同步任務

actions 類似 Vue 元件的 methods,負責改變 state呼叫 API、或執行任何非同步流程。與 mutations(Vuex)不同,Pinia 的 actions 不需要事先聲明,直接寫在 Store 中即可。

// src/stores/todoStore.js(續)
export const useTodoStore = defineStore('todo', {
  state: () => ({
    list: [] as Array<{ id: number; text: string; done: boolean }>,
    nextId: 1,
  }),

  getters: { /* 前述 getters */ },

  // 3️⃣ actions:支援同步與非同步
  actions: {
    // 新增待辦項目(同步)
    addTodo(text: string) {
      this.list.push({
        id: this.nextId++,
        text,
        done: false,
      })
    },

    // 切換完成狀態(同步)
    toggleTodo(id: number) {
      const todo = this.list.find(item => item.id === id)
      if (todo) todo.done = !todo.done
    },

    // 從遠端取得待辦清單(非同步範例)
    async fetchTodos() {
      try {
        const response = await fetch('https://api.example.com/todos')
        const data = await response.json()
        // 假設 API 回傳的資料與我們的結構相同
        this.list = data.map((item: any) => ({
          id: item.id,
          text: item.title,
          done: item.completed,
        }))
        // 更新 nextId,確保不會衝突
        this.nextId = Math.max(...this.list.map(i => i.id)) + 1
      } catch (err) {
        console.error('取得待辦清單失敗', err)
      }
    },
  },
})

注意:在 actions直接使用 this 來存取 stategetters 與其他 actions,Pinia 會自動把 this 綁定為 Store 實例,讓程式碼更直觀。


程式碼範例彙總

以下示範如何在 Vue3 元件中使用剛才建立的 Store,涵蓋 讀取、計算、觸發行為 三個層面。

<!-- src/components/TodoList.vue -->
<template>
  <section>
    <h2>待辦清單 ({{ total }})</h2>

    <input v-model="newText" @keyup.enter="add" placeholder="輸入待辦項目" />
    <button @click="add">新增</button>

    <ul>
      <li v-for="item in todos" :key="item.id">
        <label>
          <input type="checkbox" v-model="item.done" @change="toggle(item.id)" />
          <span :class="{ done: item.done }">{{ item.text }}</span>
        </label>
      </li>
    </ul>

    <p>未完成項目:{{ unfinishedCount }}</p>
    <p>已完成項目:{{ completedItems.length }}</p>
  </section>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useTodoStore } from '@/stores/todoStore'

// 1️⃣ 取得 Store 實例(唯一且可共享)
const todoStore = useTodoStore()

// 2️⃣ 直接解構 getter(自動保持響應式)
const { unfinishedCount, completedItems, totalItems: total } = todoStore

// 3️⃣ 讀取 state
const todos = todoStore.list

// 4️⃣ 本地 UI 狀態
const newText = ref('')

// 5️⃣ 呼叫 actions
function add() {
  if (newText.value.trim()) {
    todoStore.addTodo(newText.value.trim())
    newText.value = ''
  }
}
function toggle(id: number) {
  todoStore.toggleTodo(id)
}

// 6️⃣ 初始化:載入遠端資料
onMounted(() => {
  todoStore.fetchTodos()
})
</script>

<style scoped>
.done {
  text-decoration: line-through;
  color: #888;
}
</style>

範例說明

範例 重點說明
① Store 建立 defineStore 中的 state 必須是函式;getters 為計算屬性;actions 同時支援同步與非同步。
② 元件使用 透過 useTodoStore() 取得單例,直接存取 stategettersactions,保持 自動響應
③ 非同步載入 fetchTodosonMounted 中呼叫,展示 Side‑Effect(副作用)應放在 actions 裡。
④ UI 互動 addTodotoggleTodo 為純粹的同步 action,讓 UI 邏輯與資料變更分離。

常見陷阱與最佳實踐

陷阱 說明 解決方式 / 最佳實踐
1️⃣ state 直接使用物件 state 直接寫成物件,所有 Store 實例會共享同一引用,導致狀態被意外改寫。 必須使用回傳物件的函式 (state: () => ({ ... }))。
2️⃣ 在 getters 中修改 state getters 應該是純函式,修改 state 會破壞快取機制,且難以追蹤變更。 僅做計算;若需要改變 state,搬到 actions
3️⃣ 非同步程式寫在 getters getters 會被快取,非同步操作會導致不可預期的結果。 將 API 請求或延遲邏輯放在 actions,並在需要時呼叫。
4️⃣ 重複創建 Store 在同一元件內多次呼叫 defineStore(而非 useStore)會產生多個 Store,破壞單例概念。 只使用 useXXXStore() 取得已註冊的 Store。
5️⃣ 沒有類型檢查 在 TypeScript 專案中,未為 state、payload 定義類型會失去 Pinia 的型別優勢。 使用介面或 type 定義,或在 defineStore 中直接使用 as const、泛型。
6️⃣ 大型 Store 內聚過多功能 把所有功能塞進單一 Store,會讓檔案過長、維護困難。 依功能拆分多個 Store,如 useUserStoreuseProductStore,並在需要時組合使用。

其他最佳實踐

  1. 使用 persistedstate 插件:若需要在刷新頁面後保留狀態,可搭配 pinia-plugin-persistedstate
  2. 保持 Store 輕量:只放「全域需要共享」的資料,局部狀態仍建議使用元件的 ref / reactive
  3. 測試友善actions 皆為純函式(除非真的要呼叫外部服務),可以直接在單元測試中呼叫,驗證 state 變化。
  4. 命名慣例:Store 名稱使用小寫加 Store 後綴(如 useTodoStore),getter 使用名詞或描述性語句,action 使用動詞(addTodofetchTodos)。

實際應用場景

場景 為何使用 Pinia 具體實作示例
使用者登入與權限 全站多處需要存取使用者資訊與權限判斷 useAuthStore 保存 user, token, isLoggedIn,提供 login, logout, refreshToken actions。
購物車 多個商品列表、結帳頁面共用同一筆資料 useCartStore 管理 items, totalPrice,使用 getters 計算折扣、運費,actions 處理新增、刪除、結算 API。
即時聊天 訊息與線上使用者需要即時同步 useChatStore 保存 messages, onlineUsers,透過 websockets 的 connect, sendMessage actions 更新 state,getter 產生未讀訊息計數。
表單暫存 表單內容在切換路由或刷新時不遺失 useFormStore 使用 persistedstate,在每次輸入時 updateField action 更新 state,離開頁面仍可恢復。
多語系切換 全站文字需要根據使用者選擇即時變更 useLocaleStore 保存 currentLocale, messages,getter t(key) 取得對應語系文字,action setLocale 動態載入 JSON。

總結

Pinia 為 Vue3 帶來了簡潔且功能完整的狀態管理方案。透過 stategettersactions 的明確分工,我們可以:

  • 集中管理 全域資料,避免層層傳遞的繁瑣。
  • 利用 getters 產出衍生資料,保持 UI 與資料的同步且自動快取。
  • 在 actions 中 處理所有變更與副作用,讓程式碼更易測試、維護。

在實務開發中,遵守 函式化 state、純粹的 getters、行為分離的 actions,配合插件(如 persistedstate)與適當的 Store 切分,即可建立 可擴充、可測試、易除錯 的應用程式架構。

現在,您已掌握 Pinia 的核心概念與實作技巧,快把它帶入自己的 Vue3 專案,體驗更流暢的開發流程吧! 🚀