---
url: /blog-vue/problem-with-accidentally-modifying-pinia-data.md
description: 有時候使用 Pinia 會遇到資料意外變更問題，這其實不是鳳梨的錯，讓我們看看怎麼回事吧。( ´ ▽ ` )ﾉ
---

![拿著鳳梨的鱈魚](/problem-with-accidentally-modifying-pinia-data.webp){.cover}

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

相信大家用 Vue 3 後應該都改用 Pinia 了吧？沒用過的人趕快試試看吧。(´,,•ω•,,)

Pinia 最簡單的用法就像這樣：

`counter.ts`

```ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useStore = defineStore('counter', () => {
  const n = ref(0)

  return { n }
})
```

接著在要使用的地方呼叫 useStore：

`App.vue`

```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`

```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`

```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`

```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`

```vue
<script setup lang="ts">
import { useStore } from './counter.ts'

import UserCard from './UserCard.vue'

const store = useStore()
</script>

<template>
  <div>
    store price: {{ store.user.price }}
  </div>

  <hr>

  <user-card />
</template>
```

目前畫面如下圖。

![Untitled](/problem-with-accidentally-modifying-pinia-data/Untitled.png){width="200px" height="145px"}

這時候你會發現出事啦！╭(°A ,°\`)╮

按下遞增的時候，不只元件內的 user 數值發生變化，連 store 的數值也一起變啦！Σ(ˊДˋ;)

熟悉 JS 的朋友們一定都知道發生甚麼事，這是因為直接指派物件是 Call by Reference，所以：

```ts
const user = ref(store.user)
```

這個部分的程式會讓 user 依舊指向 store 的 user，結果就意外改到鳳梨裡面的資料了。(›´ω\`‹ )

這時候聰明的讀者們一定也想到解法，在 ref 的時候拷貝一次不就行了？

`UserCard.vue`

```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`

```ts
import { defineStore } from 'pinia'
import { computed, ref } 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,
  }
})
```

這樣即使元件中使用

```ts
const user = ref(store.user)
```

也不會意外修改 Pinia 中的資料了！✧⁑｡٩(ˊᗜˋ\*)و✧⁕｡

當然還有其他方式：

* [readonly](https://vuejs.org/api/reactivity-core.html#readonly)

  不過在 TypeScript 中會讓型別變得複雜，使用上會稍微有點麻煩

* [immutable.js](https://immutable-js.com/)

  功能強大，但有額外的學習、依賴成本

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

以上程式可以來這裡取得：[範例程式](https://play.pinia.vuejs.org/#eNqtVc1OGzEQfhVrL9kI8IJAVEoDoqUcygFQaW97cXYnYNj1WrY3BEU59AH6Chw49hH6PlXbt+j4J94FKpRDL4k9nvk83+eZ2UXyTko6ayEZJWNdKC4N0WBaSSomrg7yxOg8OcwFr2WjDPmiQR0zVZKpamoyoNnKYCEGb3MRPRek1XBpGgVkGb2LphUGFDV6YF2LRmi8zjkdRP90iDjjzCeDV+PGQC0rZgB3hIxLPnMLEkKl4gWMyGLh9xSBFHVGsly6iMyHuPW1ItlqHelYyzjrXZNsJp7IVs0kvdGNQIEWNigPB6gL3unzyBPJBWfWkieZW1PQ9dZENXc2mRt03ly5HqFUWQkz0zSV3mKS+7BrY6QeZVlRCvQvoeIzRQWYTMg6exFztE/36U5W8UmGF2VclDB/eg1GbJVQr4O+cj3apjt79I1Dne0G0NqiWlCUcomyGI3PNuVXz0QpmlryCtS5NByf9Yk4rKqau1NnM6qFmGJxDcXtP+w3eu6zvlCA8s2gR8swdQXGH59cnsEc1/Gwbsq2Qu9XDj+BbqrW5ujd3reixLR7fi7bj+6Nubj6rE/mBoRekbKJOjWcv5P5+BXqXbq7dLenYtcJKGFsmRKmXDzrGldN2C7RScF0k1i5WwNldLP959rPwk4Z1r4tbp+IYDX2hzYK+WBvkVXHiLaegEILJpWLaSsKS4AUVSNg/IEZdpiW+Dsidj30fx5R4YhQgpxenp9RyZSG1C39FXx67+KGtpEdNMxd7r7f41w46PNNB0GSwSZJh+Tg0F8UQxS6I3PXsodp0NcTw8hyEJ4vMNvZ3naG5dB3euTWSswMLEigdsHwmVnlcYerh3MjZMaq1mYZbIRQ6meLOwgXOquFCns3cBzpqFIPcxQfLvUkndZphzocBpwuUU8ElUQuWDn9ibve0HYl86xQ/s+Ufv46aTeA7XGvprgoFNQgDNJ2enSU/aje2PDFEiN0O6l5dA/A3ev1JPOB630xxpPWGFfjTGtUym/zhBwVFS9u0RIzdRL6J/7z9eHn40NAyHzM2oCeSA/t1+OP39+/vUALhi5XrP5WKcyk94V78W3rvm7d6smXDLfa3Fd2SUOuTtEaJxMXI7It52RPzqOIwTdZ/gUMSr+N)

## 總結 🐟

* 小心 call by reference 導致意外修改 Pinia 資料
* Pinia 提供資料時先拷貝一次，避免意外修改
