Vue3 Pinia 狀態管理:state、getters 與 actions 完整指南
簡介
在單頁應用(SPA)中,資料的共享與同步 是開發者常面臨的挑戰。Vue3 原生提供了 provide/inject、props 與 emit 等機制,但當應用規模擴大、元件層級變深時,這些方式會變得笨拙且難以維護。
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來存取state、getters與其他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() 取得單例,直接存取 state、getters、actions,保持 自動響應。 |
| ③ 非同步載入 | fetchTodos 在 onMounted 中呼叫,展示 Side‑Effect(副作用)應放在 actions 裡。 |
| ④ UI 互動 | addTodo、toggleTodo 為純粹的同步 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,如 useUserStore、useProductStore,並在需要時組合使用。 |
其他最佳實踐
- 使用
persistedstate插件:若需要在刷新頁面後保留狀態,可搭配pinia-plugin-persistedstate。 - 保持 Store 輕量:只放「全域需要共享」的資料,局部狀態仍建議使用元件的
ref/reactive。 - 測試友善:
actions皆為純函式(除非真的要呼叫外部服務),可以直接在單元測試中呼叫,驗證 state 變化。 - 命名慣例:Store 名稱使用小寫加
Store後綴(如useTodoStore),getter 使用名詞或描述性語句,action 使用動詞(addTodo、fetchTodos)。
實際應用場景
| 場景 | 為何使用 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 帶來了簡潔且功能完整的狀態管理方案。透過 state、getters、actions 的明確分工,我們可以:
- 集中管理 全域資料,避免層層傳遞的繁瑣。
- 利用 getters 產出衍生資料,保持 UI 與資料的同步且自動快取。
- 在 actions 中 處理所有變更與副作用,讓程式碼更易測試、維護。
在實務開發中,遵守 函式化 state、純粹的 getters、行為分離的 actions,配合插件(如 persistedstate)與適當的 Store 切分,即可建立 可擴充、可測試、易除錯 的應用程式架構。
現在,您已掌握 Pinia 的核心概念與實作技巧,快把它帶入自己的 Vue3 專案,體驗更流暢的開發流程吧! 🚀