誰偷了我的鳳梨!聊聊意外修改 Pinia 資料問題
相信大家用 Vue 3 後應該都改用 Pinia 了吧?沒用過的人趕快試試看吧。(´,,•ω•,,)
Pinia 最簡單的用法就像這樣:
counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useStore = defineStore('counter', () => {
const n = ref(0)
return { n }
})
接著在要使用的地方呼叫 useStore:
App.vue
<script setup lang="ts">
import { useStore } from './counter.ts'
const store = useStore()
function increment() {
store.n++;
}
</script>
<template>
<button @click="increment">
Increment {{ store.n }}
</button>
</template>
其實像這樣直接操作 store.n
的方式,方便歸方便,苓膏龜苓膏。
但不是個好做法,這種方式容易讓資料流混亂,想像一下你有多個元件都使用 store.n
,然後想改就改。…( ・ิω・ิ)
比較推薦的方式通常為由 store 提供一個修改資料的 function,例如:
counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface User {
name: string;
price: number;
}
export const useStore = defineStore('counter', () => {
const user = ref<User>({
name: 'cod',
price: 100,
})
function updateUser(data: Partial<User>) {
user.value = {
...user.value,
...data,
}
}
return {
user,
updateUser,
}
})
呼叫的部份改為:
App.vue
<script setup lang="ts">
import { useStore } from './counter.ts'
const store = useStore()
function increment() {
const price = store.user.price + 1;
store.updateUser({ price });
}
</script>
<template>
<button @click="increment">
price: {{ store.user.price }}
</button>
</template>
這樣如果未來要新增邏輯、權限甚至重構,都容易得多。
但是問題來了,有時候流程中會有「確認」的按鈕,也就是要按下確認後,才修改 store 的資料。
假設有一個負責修改 User 資料的元件:
UserCard.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useStore } from './counter.ts'
const store = useStore();
const user = ref(store.user);
function increment() {
user.value.price++;
}
function cancel() {
user.value = store.user;
}
function submit() {
store.updateUser(user.value);
}
</script>
<template>
<div>
<button class="button" @click="increment">
遞增
</button>
<button class="button" @click="submit">
確認
</button>
<div>
current price: {{ user.price }}
</div>
</div>
</template>
<style>
.button {
margin: 0px 4px;
}
</style>
App.vue
<script setup lang="ts">
import UserCard from './UserCard.vue';
import { useStore } from './counter.ts'
const store = useStore();
</script>
<template>
<div>
store price: {{ store.user.price }}
</div>
<hr />
<UserCard />
</template>
目前畫面如下圖。
這時候你會發現出事啦!╭(°A ,°`)╮
按下遞增的時候,不只元件內的 user 數值發生變化,連 store 的數值也一起變啦!Σ(ˊДˋ;)
熟悉 JS 的朋友們一定都知道發生甚麼事,這是因為直接指派物件是 Call by Reference,所以:
const user = ref(store.user);
這個部分的程式會讓 user 依舊指向 store 的 user,結果就意外改到鳳梨裡面的資料了。(›´ω`‹ )
這時候聰明的讀者們一定也想到解法,在 ref 的時候拷貝一次不就行了?
UserCard.vue
<script setup lang="ts">
...
const user = ref(clone(store.user));
function clone<Data>(data: Data): Data {
return JSON.parse(JSON.stringify(data));
}
...
</script>
...
這時候會發現世界恢復和平了,資料一切正常!◝( •ω• )◟
鱈魚:「但是阿。(´● ω ●`)」
路人:「怎麼那麼多但是?…(›´ω`‹ )」
實際上協作開發的時候難保大家都會注意到這件事情,所以保險起見,可以在 Pinia 提供資料時先拷貝一次。
counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
name: string;
price: number;
}
function clone<Data>(data: Data): Data {
return JSON.parse(JSON.stringify(data));
}
export const useStore = defineStore('counter', () => {
const user = ref<User>({
name: 'cod',
price: 100,
})
function updateUser(data: Partial<User>) {
user.value = {
...user.value,
...data,
}
}
return {
user: computed(() => clone(user.value)),
updateUser,
}
})
這樣即使元件中使用
const user = ref(store.user);
也不會意外修改 Pinia 中的資料了!✧⁑。٩(ˊᗜˋ*)و✧⁕。
當然如果使用 immutable.js 這類保證資料不變性的套件也是沒問題。
就看大家喜歡哪一種了。♪( ◜ω◝و(و
以上程式可以來這裡取得:範例程式
總結 🐟
- 小心 call by reference 導致意外修改 Pinia 資料
- Pinia 提供資料時先拷貝一次,避免意外修改