Vue3 Pinia 狀態管理 – 建立 Store(defineStore)
簡介
在單頁應用(SPA)中,元件之間的資料共享是不可避免的議題。Vue3 原生提供的 props / emit 能解決父子關係的傳遞,但面對跨層級或全局共享的需求時,狀態管理 就顯得尤為重要。Pinia 是 Vue 官方推薦的下一代狀態管理庫,取代了過去的 Vuex,語法更簡潔、類型支援更好,同時與 Vue3 的 Composition API 天生相容。
本單元將聚焦在 建立 Store——最基礎也是最關鍵的步驟。透過 defineStore,我們可以把狀態、getter、action 整理成一個獨立的模組,在任何元件中以 useStore() 的方式直接使用,讓程式碼更易讀、易維護,也更符合「單一職責」的設計原則。
核心概念
1. 為什麼使用 defineStore
defineStore 是 Pinia 提供的 API,用來 定義一個 Store。它接受兩個必填參數:
| 參數 | 型別 | 說明 |
|---|---|---|
id |
string |
Store 的唯一名稱,會成為全域註冊的 key。 |
options |
object |
包含 state、getters、actions 的設定。 |
使用 defineStore 的好處包括:
- 自動支援 TypeScript:Pinia 會根據
state、getters、actions推斷型別。 - 熱更新友好:開發時可即時看到資料變動,無需重新載入頁面。
- 模組化:每個 Store 都是獨立檔案,便於分割與測試。
2. 基本結構
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 1. state:回傳一個包含預設值的物件
state: () => ({
count: 0
}),
// 2. getters:類似 computed,依賴於 state
getters: {
doubleCount: (state) => state.count * 2,
// 若需要存取其他 getter,可使用 this
tripleCount() {
return this.doubleCount + this.count
}
},
// 3. actions:可包含非同步邏輯,直接修改 state
actions: {
increment(step = 1) {
this.count += step
},
async fetchInitialCount() {
const res = await fetch('/api/init-count')
const data = await res.json()
this.count = data.value
}
}
})
重點:
state必須是 函式,這樣每次呼叫useCounterStore()時都會得到一個獨立的狀態實例,避免共享同一個物件導致不可預期的行為。
3. 在元件中使用 Store
<template>
<div>
<p>計數:{{ counter.count }}</p>
<p>雙倍:{{ counter.doubleCount }}</p>
<button @click="counter.increment()">+1</button>
<button @click="counter.increment(5)">+5</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
// 取得 Store 實例
const counter = useCounterStore()
// 若想在組件掛載時自動呼叫非同步 action
onMounted(() => {
counter.fetchInitialCount()
})
</script>
說明:
useCounterStore()會回傳同一個 Store 實例(在同一個 Pinia 實例內),因此在多個元件間共享狀態非常直接。- 直接呼叫
counter.increment()或存取counter.doubleCount,Pinia 會自動保持 reactivity,元件會在值變動時重新渲染。
4. 多 Store 與命名空間
當專案規模變大時,建議依功能切分 Store,例如 user、cart、product。每個 Store 的 id 必須唯一,Pinia 會自動以 id 為鍵值管理。
// src/stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
profile: null
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
login(payload) {
this.token = payload.token
this.profile = payload.user
},
logout() {
this.token = ''
this.profile = null
}
}
})
在元件中同時使用多個 Store:
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const user = useUserStore()
const cart = useCartStore()
if (user.isLoggedIn) {
cart.fetchCartItems()
}
5. Store 持久化(Persist)
在真實專案中,常需要在頁面刷新後保留使用者的狀態。Pinia 官方提供 pinia-plugin-persistedstate 插件,只要在 Store 中加入 persist: true(或自訂配置),即可自動將 state 存入 localStorage 或 sessionStorage。
// src/stores/theme.js
import { defineStore } from 'pinia'
export const useThemeStore = defineStore('theme', {
state: () => ({
darkMode: false
}),
actions: {
toggle() {
this.darkMode = !this.darkMode
}
},
// 開啟持久化,預設使用 localStorage
persist: true
})
提示:若只想持久化部分欄位,可使用
persist: { paths: ['darkMode'] }。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
直接修改 state 之外的變數 |
在 actions 中若直接寫 count = 5(未加 this.),會變成局部變數,無法觸發 reactivity。 |
必須使用 this.count = 5 或 this.$state.count = 5。 |
state 不是函式 |
若寫成 state: { count: 0 },所有使用者會共享同一個物件,導致互相干擾。 |
永遠使用函式:state: () => ({ count: 0 })。 |
| Getter 內部使用 async | Getter 必須是同步的,若需要非同步計算,應改寫為 action 並把結果存回 state。 |
使用 action + state,或在組件內使用 watchEffect。 |
| 過度集中 Store | 把所有狀態都塞進一個巨大的 Store,會讓維護變得困難。 | 功能拆分:每個領域(User、Cart、Product)各自一個 Store。 |
| 忘記註冊 Pinia | 在 main.js 中沒有 app.use(createPinia()),會導致 useStore() 拋錯。 |
在入口檔案初始化 Pinia,且只初始化一次。 |
最佳實踐:
- 使用 TypeScript:即使是 JavaScript 專案,也建議加入
@pinia/nuxt或pinia的型別定義,提升開發體驗。 - 保持 Store 純粹:只放置資料與相關邏輯,避免把 UI 相關的程式碼(如 DOM 操作)寫在 Store 中。
- 利用插件:如
pinia-plugin-persistedstate、pinia-plugin-logger,可以在不改動原始程式碼的情況下加入持久化或除錯功能。 - 單元測試:Pinia Store 天然支援測試,只要呼叫
setActivePinia(createPinia())即可在 Jest/Vitest 中模擬環境。
// vitest 測試範例
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
beforeEach(() => {
setActivePinia(createPinia())
})
test('increment works', () => {
const store = useCounterStore()
store.increment(3)
expect(store.count).toBe(3)
})
實際應用場景
使用者認證與權限
userStore負責保存 JWT、使用者資訊以及登入/登出流程。- 其他需要驗證的頁面只要檢查
userStore.isLoggedIn,即可決定是否導向登入頁。
購物車系統
cartStore包含商品列表、總金額、加減商品等 actions。- 透過持久化,使用者即使關閉瀏覽器也能保留購物車內容。
即時通知
notificationStore用來管理訊息佇列,actions內部可呼叫 WebSocket,收到訊息即push到state.notifications,元件自動渲染。
主題切換
themeStore控制暗黑模式與亮色模式,結合persist後,使用者在不同頁面或重新載入時仍保持偏好。
多語系切換
localeStore保存當前語系,actions內部載入對應的 i18n 資源,所有元件透過store.currentLocale即可即時切換文字。
總結
defineStore 是 Pinia 的核心入口,透過簡潔的 id + options 結構,我們能快速建立具備 state、getter、action 的全域模組。正確使用函式式 state、遵守 reactivity 原則、適時拆分 Store,能讓專案在規模擴大時仍保持可維護性。結合插件(持久化、日志)與單元測試,更能提升開發效率與產品穩定度。
掌握了 建立 Store 後,你就擁有了 Vue3 應用中最強大的狀態管理工具,未來無論是簡單的計數器還是複雜的電商系統,都能以 Pinia 讓資料流動變得清晰、可靠。祝你在開發旅程中玩得開心,寫出更優雅的 Vue3 應用!