[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fvuex-vs-pinia":3},{"id":4,"title":5,"body":6,"category":1442,"date":1443,"description":1444,"draft":1445,"extension":1446,"meta":1447,"navigation":128,"path":1448,"readingTime":150,"seo":1449,"stem":1450,"tags":1451,"__hash__":1455},"blog\u002Fblog\u002Fvuex-vs-pinia.md","Vuex 與 Pinia 的寫法差異：從 Vue 2 到 Vue 3",{"type":7,"value":8,"toc":1428},"minimark",[9,13,16,20,71,74,77,80,176,179,268,272,275,486,489,616,627,757,760,765,768,915,928,932,1022,1025,1029,1036,1118,1122,1125,1151,1154,1212,1223,1227,1230,1233,1258,1268,1371,1377,1380,1384,1418,1421,1424],[10,11,12],"p",{},"我的 side project「遊戲揪團平台」是用 Vue 3 和 Pinia 寫的，當時只覺得狀態管理就是這樣。進公司後，因為同時接觸 Vue 2 和 Vue 3 的專案，才第一次碰到 Vuex，也才開始回頭想：為什麼它要多一層 mutation？Pinia 又為什麼拿掉了？",[10,14,15],{},"這篇用同一個計數器範例比較兩者的寫法，並記錄我從這個差異看到的事。",[17,18,19],"h2",{"id":19},"版本怎麼對應",[21,22,23,36],"table",{},[24,25,26],"thead",{},[27,28,29,33],"tr",{},[30,31,32],"th",{},"狀態管理",[30,34,35],{},"支援的 Vue 版本",[37,38,39,48,56,64],"tbody",{},[27,40,41,45],{},[42,43,44],"td",{},"Vuex 3",[42,46,47],{},"Vue 2",[27,49,50,53],{},[42,51,52],{},"Vuex 4",[42,54,55],{},"Vue 3",[27,57,58,61],{},[42,59,60],{},"Pinia 2",[42,62,63],{},"Vue 2、Vue 3",[27,65,66,69],{},[42,67,68],{},"Pinia 3",[42,70,55],{},[10,72,73],{},"換句話說，升級到 Vue 3 不一定要換掉 Vuex，Vuex 4 的 API 大致相同。但新專案建議直接使用 Pinia，Vuex 目前也處於維護狀態。",[17,75,76],{"id":76},"建立與註冊",[10,78,79],{},"Vue 2 搭配 Vuex 3 是建立實例，再傳給根元件：",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-js shiki shiki-themes github-dark","import Vue from 'vue'\nimport Vuex from 'vuex'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({ \u002F* ... *\u002F })\n","js","",[88,89,90,110,123,130,143,148],"code",{"__ignoreMap":86},[91,92,95,99,103,106],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"snl16","import",[91,100,102],{"class":101},"s95oV"," Vue ",[91,104,105],{"class":97},"from",[91,107,109],{"class":108},"sU2Wk"," 'vue'\n",[91,111,113,115,118,120],{"class":93,"line":112},2,[91,114,98],{"class":97},[91,116,117],{"class":101}," Vuex ",[91,119,105],{"class":97},[91,121,122],{"class":108}," 'vuex'\n",[91,124,126],{"class":93,"line":125},3,[91,127,129],{"emptyLinePlaceholder":128},true,"\n",[91,131,133,136,140],{"class":93,"line":132},4,[91,134,135],{"class":101},"Vue.",[91,137,139],{"class":138},"svObZ","use",[91,141,142],{"class":101},"(Vuex)\n",[91,144,146],{"class":93,"line":145},5,[91,147,129],{"emptyLinePlaceholder":128},[91,149,151,154,157,160,163,166,169,173],{"class":93,"line":150},6,[91,152,153],{"class":97},"export",[91,155,156],{"class":97}," default",[91,158,159],{"class":97}," new",[91,161,162],{"class":101}," Vuex.",[91,164,165],{"class":138},"Store",[91,167,168],{"class":101},"({ ",[91,170,172],{"class":171},"sAwPA","\u002F* ... *\u002F",[91,174,175],{"class":101}," })\n",[10,177,178],{},"Vue 3 改成函式寫法，Vuex 4 和 Pinia 都一樣：",[81,180,182],{"className":83,"code":181,"language":85,"meta":86,"style":86},"\u002F\u002F Vuex 4\nconst store = createStore({ \u002F* ... *\u002F })\ncreateApp(App).use(store).mount('#app')\n\n\u002F\u002F Pinia\ncreateApp(App).use(createPinia()).mount('#app')\n",[88,183,184,189,210,235,239,244],{"__ignoreMap":86},[91,185,186],{"class":93,"line":94},[91,187,188],{"class":171},"\u002F\u002F Vuex 4\n",[91,190,191,194,198,201,204,206,208],{"class":93,"line":112},[91,192,193],{"class":97},"const",[91,195,197],{"class":196},"sDLfK"," store",[91,199,200],{"class":97}," =",[91,202,203],{"class":138}," createStore",[91,205,168],{"class":101},[91,207,172],{"class":171},[91,209,175],{"class":101},[91,211,212,215,218,220,223,226,229,232],{"class":93,"line":125},[91,213,214],{"class":138},"createApp",[91,216,217],{"class":101},"(App).",[91,219,139],{"class":138},[91,221,222],{"class":101},"(store).",[91,224,225],{"class":138},"mount",[91,227,228],{"class":101},"(",[91,230,231],{"class":108},"'#app'",[91,233,234],{"class":101},")\n",[91,236,237],{"class":93,"line":132},[91,238,129],{"emptyLinePlaceholder":128},[91,240,241],{"class":93,"line":145},[91,242,243],{"class":171},"\u002F\u002F Pinia\n",[91,245,246,248,250,252,254,257,260,262,264,266],{"class":93,"line":150},[91,247,214],{"class":138},[91,249,217],{"class":101},[91,251,139],{"class":138},[91,253,228],{"class":101},[91,255,256],{"class":138},"createPinia",[91,258,259],{"class":101},"()).",[91,261,225],{"class":138},[91,263,228],{"class":101},[91,265,231],{"class":108},[91,267,234],{"class":101},[17,269,271],{"id":270},"定義-store最大的差別是沒有-mutations","定義 store：最大的差別是沒有 mutations",[10,273,274],{},"Vuex 規定修改 state 要透過 mutation，非同步要放在 action：",[81,276,278],{"className":83,"code":277,"language":85,"meta":86,"style":86},"export default {\n  namespaced: true,\n  state: () => ({ count: 0 }),\n  getters: {\n    double: state => state.count * 2,\n  },\n  mutations: {\n    SET_COUNT(state, value) {\n      state.count = value\n    },\n  },\n  actions: {\n    async fetchCount({ commit }) {\n      const { count } = await api.getCount()\n      commit('SET_COUNT', count)\n    },\n  },\n}\n",[88,279,280,289,300,320,325,351,356,362,381,393,399,404,410,427,456,470,475,480],{"__ignoreMap":86},[91,281,282,284,286],{"class":93,"line":94},[91,283,153],{"class":97},[91,285,156],{"class":97},[91,287,288],{"class":101}," {\n",[91,290,291,294,297],{"class":93,"line":112},[91,292,293],{"class":101},"  namespaced: ",[91,295,296],{"class":196},"true",[91,298,299],{"class":101},",\n",[91,301,302,305,308,311,314,317],{"class":93,"line":125},[91,303,304],{"class":138},"  state",[91,306,307],{"class":101},": () ",[91,309,310],{"class":97},"=>",[91,312,313],{"class":101}," ({ count: ",[91,315,316],{"class":196},"0",[91,318,319],{"class":101}," }),\n",[91,321,322],{"class":93,"line":132},[91,323,324],{"class":101},"  getters: {\n",[91,326,327,330,333,337,340,343,346,349],{"class":93,"line":145},[91,328,329],{"class":138},"    double",[91,331,332],{"class":101},": ",[91,334,336],{"class":335},"s9osk","state",[91,338,339],{"class":97}," =>",[91,341,342],{"class":101}," state.count ",[91,344,345],{"class":97},"*",[91,347,348],{"class":196}," 2",[91,350,299],{"class":101},[91,352,353],{"class":93,"line":150},[91,354,355],{"class":101},"  },\n",[91,357,359],{"class":93,"line":358},7,[91,360,361],{"class":101},"  mutations: {\n",[91,363,365,368,370,372,375,378],{"class":93,"line":364},8,[91,366,367],{"class":138},"    SET_COUNT",[91,369,228],{"class":101},[91,371,336],{"class":335},[91,373,374],{"class":101},", ",[91,376,377],{"class":335},"value",[91,379,380],{"class":101},") {\n",[91,382,384,387,390],{"class":93,"line":383},9,[91,385,386],{"class":101},"      state.count ",[91,388,389],{"class":97},"=",[91,391,392],{"class":101}," value\n",[91,394,396],{"class":93,"line":395},10,[91,397,398],{"class":101},"    },\n",[91,400,402],{"class":93,"line":401},11,[91,403,355],{"class":101},[91,405,407],{"class":93,"line":406},12,[91,408,409],{"class":101},"  actions: {\n",[91,411,413,416,419,421,424],{"class":93,"line":412},13,[91,414,415],{"class":97},"    async",[91,417,418],{"class":138}," fetchCount",[91,420,168],{"class":101},[91,422,423],{"class":335},"commit",[91,425,426],{"class":101}," }) {\n",[91,428,430,433,436,439,442,444,447,450,453],{"class":93,"line":429},14,[91,431,432],{"class":97},"      const",[91,434,435],{"class":101}," { ",[91,437,438],{"class":196},"count",[91,440,441],{"class":101}," } ",[91,443,389],{"class":97},[91,445,446],{"class":97}," await",[91,448,449],{"class":101}," api.",[91,451,452],{"class":138},"getCount",[91,454,455],{"class":101},"()\n",[91,457,459,462,464,467],{"class":93,"line":458},15,[91,460,461],{"class":138},"      commit",[91,463,228],{"class":101},[91,465,466],{"class":108},"'SET_COUNT'",[91,468,469],{"class":101},", count)\n",[91,471,473],{"class":93,"line":472},16,[91,474,398],{"class":101},[91,476,478],{"class":93,"line":477},17,[91,479,355],{"class":101},[91,481,483],{"class":93,"line":482},18,[91,484,485],{"class":101},"}\n",[10,487,488],{},"Pinia 拿掉了 mutations，action 可以直接修改 state，同步和非同步都寫在一起：",[81,490,492],{"className":83,"code":491,"language":85,"meta":86,"style":86},"export const useCounterStore = defineStore('counter', {\n  state: () => ({ count: 0 }),\n  getters: {\n    double: state => state.count * 2,\n  },\n  actions: {\n    async fetchCount() {\n      const { count } = await api.getCount()\n      this.count = count\n    },\n  },\n})\n",[88,493,494,517,531,535,553,557,561,570,590,603,607,611],{"__ignoreMap":86},[91,495,496,498,501,504,506,509,511,514],{"class":93,"line":94},[91,497,153],{"class":97},[91,499,500],{"class":97}," const",[91,502,503],{"class":196}," useCounterStore",[91,505,200],{"class":97},[91,507,508],{"class":138}," defineStore",[91,510,228],{"class":101},[91,512,513],{"class":108},"'counter'",[91,515,516],{"class":101},", {\n",[91,518,519,521,523,525,527,529],{"class":93,"line":112},[91,520,304],{"class":138},[91,522,307],{"class":101},[91,524,310],{"class":97},[91,526,313],{"class":101},[91,528,316],{"class":196},[91,530,319],{"class":101},[91,532,533],{"class":93,"line":125},[91,534,324],{"class":101},[91,536,537,539,541,543,545,547,549,551],{"class":93,"line":132},[91,538,329],{"class":138},[91,540,332],{"class":101},[91,542,336],{"class":335},[91,544,339],{"class":97},[91,546,342],{"class":101},[91,548,345],{"class":97},[91,550,348],{"class":196},[91,552,299],{"class":101},[91,554,555],{"class":93,"line":145},[91,556,355],{"class":101},[91,558,559],{"class":93,"line":150},[91,560,409],{"class":101},[91,562,563,565,567],{"class":93,"line":358},[91,564,415],{"class":97},[91,566,418],{"class":138},[91,568,569],{"class":101},"() {\n",[91,571,572,574,576,578,580,582,584,586,588],{"class":93,"line":364},[91,573,432],{"class":97},[91,575,435],{"class":101},[91,577,438],{"class":196},[91,579,441],{"class":101},[91,581,389],{"class":97},[91,583,446],{"class":97},[91,585,449],{"class":101},[91,587,452],{"class":138},[91,589,455],{"class":101},[91,591,592,595,598,600],{"class":93,"line":383},[91,593,594],{"class":196},"      this",[91,596,597],{"class":101},".count ",[91,599,389],{"class":97},[91,601,602],{"class":101}," count\n",[91,604,605],{"class":93,"line":395},[91,606,398],{"class":101},[91,608,609],{"class":93,"line":401},[91,610,355],{"class":101},[91,612,613],{"class":93,"line":406},[91,614,615],{"class":101},"})\n",[10,617,618,619,622,623,626],{},"Pinia 也可以用類似 Composition API 的寫法，",[88,620,621],{},"ref"," 就是 state，",[88,624,625],{},"computed"," 就是 getter，函式就是 action：",[81,628,630],{"className":83,"code":629,"language":85,"meta":86,"style":86},"export const useCounterStore = defineStore('counter', () => {\n  const count = ref(0)\n  const double = computed(() => count.value * 2)\n\n  async function fetchCount() {\n    count.value = (await api.getCount()).count\n  }\n\n  return { count, double, fetchCount }\n})\n",[88,631,632,655,674,700,704,716,736,741,745,753],{"__ignoreMap":86},[91,633,634,636,638,640,642,644,646,648,651,653],{"class":93,"line":94},[91,635,153],{"class":97},[91,637,500],{"class":97},[91,639,503],{"class":196},[91,641,200],{"class":97},[91,643,508],{"class":138},[91,645,228],{"class":101},[91,647,513],{"class":108},[91,649,650],{"class":101},", () ",[91,652,310],{"class":97},[91,654,288],{"class":101},[91,656,657,660,663,665,668,670,672],{"class":93,"line":112},[91,658,659],{"class":97},"  const",[91,661,662],{"class":196}," count",[91,664,200],{"class":97},[91,666,667],{"class":138}," ref",[91,669,228],{"class":101},[91,671,316],{"class":196},[91,673,234],{"class":101},[91,675,676,678,681,683,686,689,691,694,696,698],{"class":93,"line":125},[91,677,659],{"class":97},[91,679,680],{"class":196}," double",[91,682,200],{"class":97},[91,684,685],{"class":138}," computed",[91,687,688],{"class":101},"(() ",[91,690,310],{"class":97},[91,692,693],{"class":101}," count.value ",[91,695,345],{"class":97},[91,697,348],{"class":196},[91,699,234],{"class":101},[91,701,702],{"class":93,"line":132},[91,703,129],{"emptyLinePlaceholder":128},[91,705,706,709,712,714],{"class":93,"line":145},[91,707,708],{"class":97},"  async",[91,710,711],{"class":97}," function",[91,713,418],{"class":138},[91,715,569],{"class":101},[91,717,718,721,723,726,729,731,733],{"class":93,"line":150},[91,719,720],{"class":101},"    count.value ",[91,722,389],{"class":97},[91,724,725],{"class":101}," (",[91,727,728],{"class":97},"await",[91,730,449],{"class":101},[91,732,452],{"class":138},[91,734,735],{"class":101},"()).count\n",[91,737,738],{"class":93,"line":358},[91,739,740],{"class":101},"  }\n",[91,742,743],{"class":93,"line":364},[91,744,129],{"emptyLinePlaceholder":128},[91,746,747,750],{"class":93,"line":383},[91,748,749],{"class":97},"  return",[91,751,752],{"class":101}," { count, double, fetchCount }\n",[91,754,755],{"class":93,"line":395},[91,756,615],{"class":101},[17,758,759],{"id":759},"在元件裡使用",[761,762,764],"h3",{"id":763},"options-api","Options API",[10,766,767],{},"Vue 3 專案不一定都改用 Composition API。使用 Options API 時，Vuex 和 Pinia 都有對應的輔助函式，寫法很接近：",[81,769,771],{"className":83,"code":770,"language":85,"meta":86,"style":86},"\u002F\u002F Vuex\ncomputed: {\n  ...mapState('counter', ['count']),\n  ...mapGetters('counter', ['double']),\n},\nmethods: {\n  ...mapActions('counter', ['fetchCount']),\n},\n\n\u002F\u002F Pinia\ncomputed: {\n  ...mapState(useCounterStore, ['count', 'double']),\n},\nmethods: {\n  ...mapActions(useCounterStore, ['fetchCount']),\n},\n",[88,772,773,778,785,806,824,829,836,854,858,862,866,872,889,893,899,911],{"__ignoreMap":86},[91,774,775],{"class":93,"line":94},[91,776,777],{"class":171},"\u002F\u002F Vuex\n",[91,779,780,782],{"class":93,"line":112},[91,781,625],{"class":138},[91,783,784],{"class":101},": {\n",[91,786,787,790,793,795,797,800,803],{"class":93,"line":125},[91,788,789],{"class":97},"  ...",[91,791,792],{"class":138},"mapState",[91,794,228],{"class":101},[91,796,513],{"class":108},[91,798,799],{"class":101},", [",[91,801,802],{"class":108},"'count'",[91,804,805],{"class":101},"]),\n",[91,807,808,810,813,815,817,819,822],{"class":93,"line":132},[91,809,789],{"class":97},[91,811,812],{"class":138},"mapGetters",[91,814,228],{"class":101},[91,816,513],{"class":108},[91,818,799],{"class":101},[91,820,821],{"class":108},"'double'",[91,823,805],{"class":101},[91,825,826],{"class":93,"line":145},[91,827,828],{"class":101},"},\n",[91,830,831,834],{"class":93,"line":150},[91,832,833],{"class":138},"methods",[91,835,784],{"class":101},[91,837,838,840,843,845,847,849,852],{"class":93,"line":358},[91,839,789],{"class":97},[91,841,842],{"class":138},"mapActions",[91,844,228],{"class":101},[91,846,513],{"class":108},[91,848,799],{"class":101},[91,850,851],{"class":108},"'fetchCount'",[91,853,805],{"class":101},[91,855,856],{"class":93,"line":364},[91,857,828],{"class":101},[91,859,860],{"class":93,"line":383},[91,861,129],{"emptyLinePlaceholder":128},[91,863,864],{"class":93,"line":395},[91,865,243],{"class":171},[91,867,868,870],{"class":93,"line":401},[91,869,625],{"class":138},[91,871,784],{"class":101},[91,873,874,876,878,881,883,885,887],{"class":93,"line":406},[91,875,789],{"class":97},[91,877,792],{"class":138},[91,879,880],{"class":101},"(useCounterStore, [",[91,882,802],{"class":108},[91,884,374],{"class":101},[91,886,821],{"class":108},[91,888,805],{"class":101},[91,890,891],{"class":93,"line":412},[91,892,828],{"class":101},[91,894,895,897],{"class":93,"line":429},[91,896,833],{"class":138},[91,898,784],{"class":101},[91,900,901,903,905,907,909],{"class":93,"line":458},[91,902,789],{"class":97},[91,904,842],{"class":138},[91,906,880],{"class":101},[91,908,851],{"class":108},[91,910,805],{"class":101},[91,912,913],{"class":93,"line":472},[91,914,828],{"class":101},[10,916,917,918,920,921,923,924,927],{},"要注意 Pinia 沒有 ",[88,919,812],{}," 的必要，getter 也用 ",[88,922,792],{}," 取得。如果要在元件裡修改 state，要改用 ",[88,925,926],{},"mapWritableState","。",[761,929,931],{"id":930},"composition-api","Composition API",[81,933,935],{"className":83,"code":934,"language":85,"meta":86,"style":86},"\u002F\u002F Vuex 4\nconst store = useStore()\nconst count = computed(() => store.state.counter.count)\nstore.dispatch('counter\u002FfetchCount')\n\n\u002F\u002F Pinia\nconst counter = useCounterStore()\ncounter.count\ncounter.fetchCount()\n",[88,936,937,941,954,971,986,990,994,1007,1012],{"__ignoreMap":86},[91,938,939],{"class":93,"line":94},[91,940,188],{"class":171},[91,942,943,945,947,949,952],{"class":93,"line":112},[91,944,193],{"class":97},[91,946,197],{"class":196},[91,948,200],{"class":97},[91,950,951],{"class":138}," useStore",[91,953,455],{"class":101},[91,955,956,958,960,962,964,966,968],{"class":93,"line":125},[91,957,193],{"class":97},[91,959,662],{"class":196},[91,961,200],{"class":97},[91,963,685],{"class":138},[91,965,688],{"class":101},[91,967,310],{"class":97},[91,969,970],{"class":101}," store.state.counter.count)\n",[91,972,973,976,979,981,984],{"class":93,"line":132},[91,974,975],{"class":101},"store.",[91,977,978],{"class":138},"dispatch",[91,980,228],{"class":101},[91,982,983],{"class":108},"'counter\u002FfetchCount'",[91,985,234],{"class":101},[91,987,988],{"class":93,"line":145},[91,989,129],{"emptyLinePlaceholder":128},[91,991,992],{"class":93,"line":150},[91,993,243],{"class":171},[91,995,996,998,1001,1003,1005],{"class":93,"line":358},[91,997,193],{"class":97},[91,999,1000],{"class":196}," counter",[91,1002,200],{"class":97},[91,1004,503],{"class":138},[91,1006,455],{"class":101},[91,1008,1009],{"class":93,"line":364},[91,1010,1011],{"class":101},"counter.count\n",[91,1013,1014,1017,1020],{"class":93,"line":383},[91,1015,1016],{"class":101},"counter.",[91,1018,1019],{"class":138},"fetchCount",[91,1021,455],{"class":101},[10,1023,1024],{},"Pinia 少了字串路徑，打錯名稱時編輯器和 TypeScript 比較容易提示。",[17,1026,1028],{"id":1027},"最常踩的坑解構失去響應式","最常踩的坑：解構失去響應式",[10,1030,1031,1032,1035],{},"store 是 reactive 物件，直接解構 state 會失去響應式。要用 ",[88,1033,1034],{},"storeToRefs","，action 則可以直接解構：",[81,1037,1039],{"className":83,"code":1038,"language":85,"meta":86,"style":86},"const counter = useCounterStore()\n\nconst { count } = counter \u002F\u002F 錯誤：不會隨 store 更新\nconst { count, double } = storeToRefs(counter) \u002F\u002F 正確\nconst { fetchCount } = counter \u002F\u002F action 可以直接解構\n",[88,1040,1041,1053,1057,1075,1101],{"__ignoreMap":86},[91,1042,1043,1045,1047,1049,1051],{"class":93,"line":94},[91,1044,193],{"class":97},[91,1046,1000],{"class":196},[91,1048,200],{"class":97},[91,1050,503],{"class":138},[91,1052,455],{"class":101},[91,1054,1055],{"class":93,"line":112},[91,1056,129],{"emptyLinePlaceholder":128},[91,1058,1059,1061,1063,1065,1067,1069,1072],{"class":93,"line":125},[91,1060,193],{"class":97},[91,1062,435],{"class":101},[91,1064,438],{"class":196},[91,1066,441],{"class":101},[91,1068,389],{"class":97},[91,1070,1071],{"class":101}," counter ",[91,1073,1074],{"class":171},"\u002F\u002F 錯誤：不會隨 store 更新\n",[91,1076,1077,1079,1081,1083,1085,1088,1090,1092,1095,1098],{"class":93,"line":132},[91,1078,193],{"class":97},[91,1080,435],{"class":101},[91,1082,438],{"class":196},[91,1084,374],{"class":101},[91,1086,1087],{"class":196},"double",[91,1089,441],{"class":101},[91,1091,389],{"class":97},[91,1093,1094],{"class":138}," storeToRefs",[91,1096,1097],{"class":101},"(counter) ",[91,1099,1100],{"class":171},"\u002F\u002F 正確\n",[91,1102,1103,1105,1107,1109,1111,1113,1115],{"class":93,"line":145},[91,1104,193],{"class":97},[91,1106,435],{"class":101},[91,1108,1019],{"class":196},[91,1110,441],{"class":101},[91,1112,389],{"class":97},[91,1114,1071],{"class":101},[91,1116,1117],{"class":171},"\u002F\u002F action 可以直接解構\n",[17,1119,1121],{"id":1120},"模組與-store-之間的呼叫","模組與 store 之間的呼叫",[10,1123,1124],{},"Vuex 只有一個 store，再用 modules 拆分，呼叫其他模組要靠字串路徑：",[81,1126,1128],{"className":83,"code":1127,"language":85,"meta":86,"style":86},"dispatch('user\u002FfetchProfile', null, { root: true })\n",[88,1129,1130],{"__ignoreMap":86},[91,1131,1132,1134,1136,1139,1141,1144,1147,1149],{"class":93,"line":94},[91,1133,978],{"class":138},[91,1135,228],{"class":101},[91,1137,1138],{"class":108},"'user\u002FfetchProfile'",[91,1140,374],{"class":101},[91,1142,1143],{"class":196},"null",[91,1145,1146],{"class":101},", { root: ",[91,1148,296],{"class":196},[91,1150,175],{"class":101},[10,1152,1153],{},"Pinia 沒有 modules，每個 store 各自獨立，需要時直接引入：",[81,1155,1157],{"className":83,"code":1156,"language":85,"meta":86,"style":86},"actions: {\n  async checkout() {\n    const user = useUserStore()\n    await user.fetchProfile()\n  },\n}\n",[88,1158,1159,1166,1176,1191,1204,1208],{"__ignoreMap":86},[91,1160,1161,1164],{"class":93,"line":94},[91,1162,1163],{"class":138},"actions",[91,1165,784],{"class":101},[91,1167,1168,1171,1174],{"class":93,"line":112},[91,1169,1170],{"class":101},"  async ",[91,1172,1173],{"class":138},"checkout",[91,1175,569],{"class":101},[91,1177,1178,1181,1184,1186,1189],{"class":93,"line":125},[91,1179,1180],{"class":97},"    const",[91,1182,1183],{"class":196}," user",[91,1185,200],{"class":97},[91,1187,1188],{"class":138}," useUserStore",[91,1190,455],{"class":101},[91,1192,1193,1196,1199,1202],{"class":93,"line":132},[91,1194,1195],{"class":97},"    await",[91,1197,1198],{"class":101}," user.",[91,1200,1201],{"class":138},"fetchProfile",[91,1203,455],{"class":101},[91,1205,1206],{"class":93,"line":145},[91,1207,355],{"class":101},[91,1209,1210],{"class":93,"line":150},[91,1211,485],{"class":101},[10,1213,1214,1215,1218,1219,1222],{},"要注意 ",[88,1216,1217],{},"useUserStore()"," 要在函式裡呼叫，不能寫在檔案最上層。在 router guard 這類元件外的地方也一樣，要等 ",[88,1220,1221],{},"app.use(pinia)"," 之後才能使用。",[17,1224,1226],{"id":1225},"我的體會vuex-為什麼比較繁瑣","我的體會：Vuex 為什麼比較繁瑣",[10,1228,1229],{},"在揪團平台裡，我用 Setup Store 管理多步驟的建立揪團表單，再搭配持久化外掛保留資料。要改狀態就直接改，寫起來很直覺。",[10,1231,1232],{},"接觸公司的 Vuex 專案後，我的第一個感覺就是繁瑣。同樣是修改一個值，Pinia 可以直接改：",[81,1234,1236],{"className":83,"code":1235,"language":85,"meta":86,"style":86},"const counter = useCounterStore()\ncounter.count++\n",[88,1237,1238,1250],{"__ignoreMap":86},[91,1239,1240,1242,1244,1246,1248],{"class":93,"line":94},[91,1241,193],{"class":97},[91,1243,1000],{"class":196},[91,1245,200],{"class":97},[91,1247,503],{"class":138},[91,1249,455],{"class":101},[91,1251,1252,1255],{"class":93,"line":112},[91,1253,1254],{"class":101},"counter.count",[91,1256,1257],{"class":97},"++\n",[10,1259,1260,1261,1263,1264,1267],{},"Vuex 則要先定義 mutation，再從元件呼叫它。使用 Options API 時，可以在 ",[88,1262,833],{}," 裡對應進來，或直接用 ",[88,1265,1266],{},"this.$store.commit","，公司專案裡兩種寫法我都看過：",[81,1269,1271],{"className":83,"code":1270,"language":85,"meta":86,"style":86},"\u002F\u002F store\nmutations: {\n  INCREMENT(state) {\n    state.count++\n  },\n},\n\n\u002F\u002F 元件：用 mapMutations 對應到 methods\nmethods: {\n  ...mapMutations('counter', ['INCREMENT']),\n},\n\n\u002F\u002F 元件：或直接 commit\nthis.$store.commit('counter\u002FINCREMENT')\n",[88,1272,1273,1278,1285,1293,1300,1304,1308,1312,1317,1323,1341,1345,1349,1354],{"__ignoreMap":86},[91,1274,1275],{"class":93,"line":94},[91,1276,1277],{"class":171},"\u002F\u002F store\n",[91,1279,1280,1283],{"class":93,"line":112},[91,1281,1282],{"class":138},"mutations",[91,1284,784],{"class":101},[91,1286,1287,1290],{"class":93,"line":125},[91,1288,1289],{"class":138},"  INCREMENT",[91,1291,1292],{"class":101},"(state) {\n",[91,1294,1295,1298],{"class":93,"line":132},[91,1296,1297],{"class":101},"    state.count",[91,1299,1257],{"class":97},[91,1301,1302],{"class":93,"line":145},[91,1303,355],{"class":101},[91,1305,1306],{"class":93,"line":150},[91,1307,828],{"class":101},[91,1309,1310],{"class":93,"line":358},[91,1311,129],{"emptyLinePlaceholder":128},[91,1313,1314],{"class":93,"line":364},[91,1315,1316],{"class":171},"\u002F\u002F 元件：用 mapMutations 對應到 methods\n",[91,1318,1319,1321],{"class":93,"line":383},[91,1320,833],{"class":138},[91,1322,784],{"class":101},[91,1324,1325,1327,1330,1332,1334,1336,1339],{"class":93,"line":395},[91,1326,789],{"class":97},[91,1328,1329],{"class":138},"mapMutations",[91,1331,228],{"class":101},[91,1333,513],{"class":108},[91,1335,799],{"class":101},[91,1337,1338],{"class":108},"'INCREMENT'",[91,1340,805],{"class":101},[91,1342,1343],{"class":93,"line":401},[91,1344,828],{"class":101},[91,1346,1347],{"class":93,"line":406},[91,1348,129],{"emptyLinePlaceholder":128},[91,1350,1351],{"class":93,"line":412},[91,1352,1353],{"class":171},"\u002F\u002F 元件：或直接 commit\n",[91,1355,1356,1359,1362,1364,1366,1369],{"class":93,"line":429},[91,1357,1358],{"class":196},"this",[91,1360,1361],{"class":101},".$store.",[91,1363,423],{"class":138},[91,1365,228],{"class":101},[91,1367,1368],{"class":108},"'counter\u002FINCREMENT'",[91,1370,234],{"class":101},[10,1372,1373,1374,1376],{},"需要非同步時還要再多寫一個 action，由 action 去 ",[88,1375,423],{},"。整個流程多了好幾個地方要寫。",[10,1378,1379],{},"後來我才理解這層規定的目的：把所有修改集中在 mutation，狀態的變化就有固定的入口，比較容易追蹤。Pinia 拿掉這層後寫起來更自由，但任何地方都能直接改 state，所以規範要由開發者自己維持，例如把複雜的修改集中在 action 裡。",[17,1381,1383],{"id":1382},"如果要從-vuex-換到-pinia","如果要從 Vuex 換到 Pinia",[1385,1386,1387,1394,1397,1400,1405,1411],"ul",{},[1388,1389,1390,1391,1393],"li",{},"把 ",[88,1392,423],{}," 改成直接修改 state，或呼叫 action。",[1388,1395,1396],{},"把字串路徑改成 store 函式。",[1388,1398,1399],{},"把一個大 store 的 modules 拆成多個獨立 store。",[1388,1401,1402,1403,927],{},"解構 state 的地方都改用 ",[88,1404,1034],{},[1388,1406,1407,1410],{},[88,1408,1409],{},"$reset()"," 只有 Option Store 內建，Setup Store 要自己寫重置函式。",[1388,1412,1413,1414,1417],{},"持久化改用 ",[88,1415,1416],{},"pinia-plugin-persistedstate"," 這類 Pinia 的外掛。",[17,1419,1420],{"id":1420},"小結",[10,1422,1423],{},"Vuex 的重點是流程要統一，所有修改都走 mutation。Pinia 的重點是寫法更少、型別更好，把 store 當成一般的函式和資料來用。先用過 Pinia 再回頭看 Vuex，讓我理解到一個框架的「麻煩」，常常是在替團隊解決另一個問題。",[1425,1426,1427],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":86,"searchDepth":112,"depth":112,"links":1429},[1430,1431,1432,1433,1437,1438,1439,1440,1441],{"id":19,"depth":112,"text":19},{"id":76,"depth":112,"text":76},{"id":270,"depth":112,"text":271},{"id":759,"depth":112,"text":759,"children":1434},[1435,1436],{"id":763,"depth":125,"text":764},{"id":930,"depth":125,"text":931},{"id":1027,"depth":112,"text":1028},{"id":1120,"depth":112,"text":1121},{"id":1225,"depth":112,"text":1226},{"id":1382,"depth":112,"text":1383},{"id":1420,"depth":112,"text":1420},"vue","2025-08-20","比較 Vuex 3、Vuex 4 與 Pinia 在建立 store、元件取用、模組拆分上的寫法，並整理遷移時容易出錯的地方。",false,"md",{},"\u002Fblog\u002Fvuex-vs-pinia",{"title":5,"description":1444},"blog\u002Fvuex-vs-pinia",[1452,1453,1454],"Vuex","Pinia","Migration","iRV92aLVNj9tI-boGbSGmm4UehFmRa4U6koAMevoGOA"]