從 Pinia 開始:Vue 3 狀態管理實戰指南

你是否在 Vue 3 專案裡,被跨元件的資料傳遞搞得一個頭兩個大?父元件傳 props 給子元件、子元件用 emit 回傳,一層又一層地「打電話轉接」,只為了讓深層的某個按鈕能讀到頂層的登入狀態或購物車清單。這種「prop 鑽孔(prop drilling)」的痛苦,每個中大型前端專案都逃不掉。

這時候你需要的,不是更複雜的元件設計,而是一個專門管理「全域狀態」的中央倉庫——Pinia。它是 Vue 官方目前推薦的狀態管理方案,取代了過去的 Vuex,對 TypeScript 極度友善,學習曲線也更平緩。

一、Pinia 是什麼?和 Vuex 差在哪?

Pinia 的本質是一個「狀態容器」:把需要跨元件共享的資料(state)、根據資料計算出來的值(getters)、以及修改資料的行為(actions)集中管理。

相較於 Vuex,Pinia 做了幾個讓人鬆一口氣的簡化:

  • 去掉 mutations:Vuex 裡改狀態要寫 mutation、呼叫要寫 action,雙層結構很囉唆;Pinia 直接讓 action 改 state。
  • 對 TypeScript 原生友好:不需要到處寫 as 型別推斷,開發體驗順滑。
  • 模組化更直覺:每個 store 就是一個獨立檔案,用 defineStore 定義,想拆多細就拆多細。

二、建立第一個 Store

我們用一個最經典的「計數器」來走一遍完整流程。

步驟 1:安裝 Pinia

npm install pinia

步驟 2:在入口檔註冊

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

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

步驟 3:定義你的第一個 store

// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

三、在元件中使用 Store

<script setup> 裡呼叫 store 函式即可,使用方式和普通的響應式物件幾乎一樣。

步驟 1:引入並建立 store 實例

import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()

步驟 2:讀取狀態與 getter

<template>
  <p>計數:{{ counter.count }}</p>
  <p>兩倍:{{ counter.double }}</p>
</template>

步驟 3:呼叫 action 修改狀態

<button @click="counter.increment">+1</button>

重點:如果你要把 state 解構出來用(例如 const { count } = counter),記得用 storeToRefs 包裹,否則會失去響應式:

import { storeToRefs } from 'pinia'
const { count, double } = storeToRefs(counter)

四、實戰進階:拆多個 Store 管理複雜業務

真實專案很少只有一個 store。建議按「業務領域」拆分,例如 useUserStore(登入使用者)、useCartStore(購物車)、useOrderStore(訂單)。不同 store 之間也能互相呼叫:

// stores/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  actions: {
    checkout() {
      const user = useUserStore()
      if (!user.isLogin) {
        alert('請先登入再結帳')
        return
      }
      // ...呼叫 API 下單
    },
  },
})

五、常見坑與最佳實踐

  • 重點:永遠用 storeToRefs 解構 state 與 getters,直接解構會斷開響應式連結。
  • 重點:修改狀態盡量走 action,不要在元件裡直接 counter.count++,便於追蹤與除錯。
  • 大型專案把 store 按領域拆分,避免單一 store 肥大難維護。
  • Pinia 內建 devtools 支援,開發時直接在瀏覽器面板檢查每個 store 的狀態變化,非常好用。

互動話題:你在專案中是怎麼管理跨元件狀態的?是直接用 Pinia,還是還停留在 provide/inject 或 Event Bus?歡迎在留言區分享你的做法與踩過的坑!