跳到主要內容
其他筆記

Vuex API 語法速查:安裝、state、mutations、actions、getters、modules

整理當時上課做的一系列 Vuex 練習筆記——安裝與注入 store、state 的兩種取用方式、mutations 的三種 commit 傳參方式、actions 裡串接多個非同步動作、getters 回傳值/參數/函式三種型態、modules 拆分,以及 v-model 搭配 store 狀態的 get/set 寫法。核心概念與 Pinia 比較另外整理在別篇,這篇純粹是語法速查。

W

這篇是上課期間陸續做的 Vuex 練習筆記合輯,涵蓋安裝到 state/mutations/actions/getters/modules 每一塊的具體語法。核心概念的說明、Vuex 遇到的瓶頸、跟 Pinia 的完整比較已經整理在《Pinia 與 Vuex:差在哪、怎麼選、怎麼寫,這篇不重複講「為什麼」,純粹當語法查詢用;一個從頭到尾能動的練習範例見《Vuex 上課筆記:用一個 todos 範例串起 state、mutation、action》。

安裝與注入

npm install vuex --save
// main.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

store/index.js(彙整 Vuex 所有行為的地方):

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
});

main.js 裡注入:

import store from './store';

new Vue({
  store,
});

一定要在最上層(main.js)注入 store,之後元件裡用 mapStatemapMutationsmapActions 這些輔助函式才會生效,沒注入的話這些輔助函式拿不到 store 實例。

Store 三大規則

  1. app 層級的狀態集中放進 store——例如會員登入狀態、跨元件都會用到的商品列表
  2. mutations 是改變 state 的唯一方法,且必須是同步的
  3. 非同步操作都放在 actions 裡

state:儲存狀態

state 裡可以放任何東西,但要事先定義好欄位,就算給空值也沒關係:

state: {
  loading: false,
  todos: [],
}

沒有事先定義、想動態加欄位的話要用 Vue.set(state, 'loading', false),但這不是建議的做法——事先定義好結構,可讀性跟可維護性都比較好。

取用 state 一定要透過 computed(不要在 methods 或直接在模板裡運算),這樣資料變動時畫面才會自動反應:

// 第一種:直接用 $store
computed: {
  loading() {
    return this.$store.state.loading;
  },
}
// 第二種:用 mapState
import { mapState } from 'vuex';

computed: {
  ...mapState(['loading']),
}

mutations:唯一能改變 state 的地方

mutations: {
  SET_LOADING(state, value) {
    state.loading = value;
  },
}

必須是同步操作,不能在 mutation 裡直接呼叫 API——因為 Vuex 的 devtools 是靠追蹤每一次 mutation 來記錄狀態變化的時間點,非同步操作會讓這個追蹤失去意義(沒辦法確定狀態是在哪個時間點被改變的)。命名習慣上,mutation 常用全大寫命名,跟一般方法區分開來。

在元件裡觸發 mutation 用 commit,有三種傳參方式:

// 傳入字串(不帶參數)
this.$store.commit('SET_LOADING');

// 傳入參數(payload)
this.$store.commit('SET_LOADING', true);

// 傳入物件(type 欄位指定要觸發哪個 mutation)
this.$store.commit({
  type: 'SET_LOADING',
  value: true,
});

mapMutations 映射到 methods

import { mapMutations } from 'vuex';

methods: {
  // 字串陣列方式
  ...mapMutations(['SET_LOADING']),

  // 物件別名方式(本地方法名稱不同於 mutation 名稱時)
  ...mapMutations({
    setLoading: 'SET_LOADING',
  }),
}

actions:非同步操作

actions: {
  async loadTodos(context) {
    const res = await axios.get('/api/todos');
    context.commit('SET_TODOS', res.data); // 非同步完成後才 commit
  },
}

action 不能直接修改 state,一定要透過 commit 觸發 mutation——這是 Vuex 架構上的硬性規則,就算 action 裡已經非同步取得資料,也不能跳過 mutation 直接 state.todos = res.data

在元件裡觸發 action 用 dispatch

this.$store.dispatch('loadTodos');

// 帶參數
this.$store.dispatch('loadTodos', { page: 2 });

// 物件別名寫法
this.$store.dispatch({ type: 'loadTodos', page: 2 });

action 裡面做多次 commit 是同步依序執行的(一行一行往下跑,不會因為前一個 commit 是同步操作而互相打斷)。

mapActions 用法跟 mapMutations 對稱:

import { mapActions } from 'vuex';

methods: {
  ...mapActions(['loadTodos']),
  ...mapActions({ loadMyTodos: 'loadTodos' }), // 物件別名
}

action 完成後呼叫另一個 action、或需要等待非同步結果,靠回傳 Promise 串接:

actions: {
  async loadTodos(context) {
    const res = await axios.get('/api/todos');
    context.commit('SET_TODOS', res.data);
    return res.data; // 回傳值,讓外部可以用 .then() 接
  },
}
this.$store.dispatch('loadTodos').then((todos) => {
  console.log('載入完成', todos);
});

getters:store 層級的 computed

getters: {
  // 直接依 state 回傳值
  todoCount(state) {
    return state.todos.length;
  },

  // 回傳參數(依賴其他 getter)
  doneTodoCount(state, getters) {
    return getters.todoCount - state.todos.filter((t) => !t.done).length;
  },

  // 回傳一個函式,讓呼叫端自己帶參數
  getTodoById(state) {
    return (id) => state.todos.find((t) => t.id === id);
  },
}

getters 一定是吃 state 當第一個參數,第二個參數可以拿到其他 getters回傳函式這個型態很實用——一般的 getter 結果會被快取(依賴不變就不重新計算),但回傳函式的寫法每次呼叫都會重新執行,適合需要外部傳參數查詢的情境(例如「依 ID 查某一筆」)。

在元件裡使用:

// 直接用 $store.getters
computed: {
  todoCount() {
    return this.$store.getters.todoCount;
  },
}
// mapGetters,也支援物件別名
import { mapGetters } from 'vuex';

computed: {
  ...mapGetters(['todoCount']),
  ...mapGetters({ count: 'todoCount' }),
}

getTodoById 這種回傳函式的 getter,在元件裡當函式呼叫:

this.$store.getters.getTodoById(3);

modules:拆分大型 store

// store/modules/todos.js
export default {
  state: { todos: [] },
  mutations: {
    SET_TODOS(state, value) {
      state.todos = value;
    },
  },
  actions: {
    async loadTodos({ commit }) {
      const res = await axios.get('/api/todos');
      commit('SET_TODOS', res.data);
    },
  },
};
// store/index.js
import todos from './modules/todos';

export default new Vuex.Store({
  modules: { todos },
});

拆成 module 之後,state 的存取路徑要多一層模組名稱:

this.$store.state.todos.todos; // 模組名稱.state 欄位名稱

v-model 搭配 store 狀態:getter/setter computed

v-model 需要同時讀取跟寫入,直接綁 $store.state.xxx 沒辦法寫入(state 不能被直接賦值),要包一層帶 getsetcomputed

computed: {
  keyword: {
    get() {
      return this.$store.state.keyword;
    },
    set(value) {
      this.$store.commit('SET_KEYWORD', value);
    },
  },
}
<input v-model="keyword">

畫面上看起來就是普通的雙向綁定,實際上讀取跟寫入分別走 state 讀取與 commit 觸發 mutation 兩條路徑,符合「只有 mutation 能改 state」的規則。

現在還適用嗎

這篇整理的全部語法對既有的 Vuex 專案依然完全有效,Vuex 沒有被移除、也還在維護。但新專案的建議已經是 Pinia——getter/setter computed 這種為了配合 v-model 而繞的寫法,在 Pinia 裡不需要,直接讀寫 state 就好;mutations 這一整層在 Pinia 裡也被拿掉了。完整比較看《Pinia 與 Vuex:差在哪、怎麼選、怎麼寫》。

參考資料

分享這篇