2024-06-30|閱讀時間 ‧ 約 9 分鐘

誰偷了我的鳳梨!聊聊意外修改 Pinia 資料問題

鱈魚的魚缸搬家了!新家文章皆有重新修訂,歡迎來新家看看喔。(´▽`ʃ♡ƪ)
raw-image


相信大家用 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 這類保證資料不變性的套件也是沒問題。


就看大家喜歡哪一種了。♪( ◜ω◝و(و


以上程式可以來這裡取得:範例程式

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.