---
# System prepended metadata

title: Pinia
tags: [Vue]

---

# Pinia

## References
+ 🔗 [**Pinia**](https://pinia.vuejs.org/)
+ 🔗 [**liubing - 是时候放弃 Vuex 使用大菠萝 Pinia 了**](https://liubing.me/article/vue/study-pinia.html)
+ 🔗 [**Vue 3 Toast notifications using Pinia and DaisyUI**](https://gist.github.com/JavascriptMick/d2830e8703e706fe0a0f6703b0877f3f)
+ 🔗 [**Alex Liu - 深入淺出 pinia**](https://mini-ghost.dev/posts/pinia-source-code-1)

## Note

|☢️ <span class="warning">WARNING</span>|
|:---|
|<mark>pinia</mark> 的 `use~()` 回傳的那坨東西，<br />實際上是一個很多 RefImpl / ... (裡面可能還有 Proxy) 包起來的<mark>更大的 Proxy</mark>，<br />而不是像 <mark>`ref` 一個純物件</mark>那樣，是一個 Proxy 放在 `.value` 的 <mark>RefImpl</mark>。<br />假如今天你有一個 request 的 payload 是一個純物件，<br />那麼直接將那坨東西丟給 axios 是不行的，<br />會導致 `RangeError: Maximum call stack size exceeded`<br />(<mark>而且在 prod 時期才會跳出來，dev 時期是不會跳錯誤的。<br />OMG，你知道這個讓我 debug 多久嗎？</mark>)。<br />所以說，你可能要自己想辦法將那坨東西，轉成可以放在 payload 的物件才行。<br />(註：axios 可接受純物件、由 `reactive()` 產生出來的 Proxy、由 `ref()` 產生出來的 RefImpl)|

## Why Pinia?

+ 這樣寫 `userName` 確實也是全域狀態，為啥我還需要 Pinia？

  ```ts
  // composables/useUser.ts
  const userName = ref('')

  export default function useUser() {
    const setUserName = (name: string) => {
      userName.value = name
    }

    return {
      userName: readonly(userName),
      setUserName
    }
  }
  ```

+ 問題在哪？
  + 若使用 CSR，這沒有問題
    + 即便你不使用 SSR，我也不建議你這樣寫！
  + 若使用 SSR，這會導致 <mark>server 與 client 的全域狀態不一致或汙染</mark> (引發 [Hydration Error])
    + 因為<mark>這全域狀態是 server 的整個應用程式共享 (即在所有 request 間共享</mark>)

+ 在所有 request 間共享是啥意思？
  + 假設有兩個使用者請求同一個 Nuxt 頁面：
    1. 使用者 A 進入網站
        + `userName` 的初始值是 `""`
        + 將 `userName` 設為 `"Alice"`
    2. 使用者 B 進入網站，發送新的 request
        + 由於 `userName` 在所有 request 間共享，所以它仍然是 `"Alice"`，而不是 `""`
    3. 結果
        + B 看到的是 A 的 `userName` (全域狀態汙染)

+ 解決方式
  + ❌ 把 `userName` 移到函式內 (但這樣就不是全域狀態了)
  + ✅ 使用 Pinia (自動處理 SSR 相關問題)
  + ✅ 使用 Nuxt 的 `useState` (自動處理 SSR 相關問題)

## Init

```js
import { createApp } from 'vue'
import { createPinia } from 'pinia' # ✅
import App from './App.vue'

const pinia = createPinia() # ✅
const app = createApp(App)

app.use(pinia) # ✅
app.mount('#app')
```

## Similar

#### Options API
+ `state` : is similar to `data`
+ `getters` : is similar to `computed`
+ `actions` : is similar to `methods`

#### Composition API
+ `ref()` : is similar to `state`
+ `computed()` : is similar to `getters`
+ `function()` : is similar to `actions`

## Store

#### Options API
  + `defineStore()`
    ```js
    import { defineStore } from 'pinia'
    export const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0, name: 'Eduardo' }),
      getters: {
        doubleCount: (state) => state.count * 2,
      },
      actions: {
        increment() {
          this.count++
        },
      },
    })
    ```

#### Composition API
  + `defineStore()`
    ```js
    import { defineStore } from 'pinia'
    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0)
      const name = ref('Eduardo')
      const doubleCount = computed(() => count.value * 2)
      function increment() {
        count.value++
      }

      return { count, name, doubleCount, increment }
    })
    ```

## Using the Store

#### Options API
  + `mapState()` `mapActions()`
      ```js
      // 匯入 states 和 getters
      import { mapState } from 'pinia'
      // 匯入 actions
      import { mapActions } from 'pinia'
      import userStore from '@/stores/user'

      export default {
        computed: {
          ...mapState(userStore, ['count', 'name', 'doubleCount'])
        },
        methods: {
          ...mapActions(userStore, ['increment'])
        }
      }
      ```
#### Composition API
  + `import`
    ```html
    <template>
      <div>
        {{ counter.count }}
        {{ counter.doubleCount }}
        <button @click="counter.count++">+</button>
      </div>
    </template>

    <script setup>
    import { useCounterStore } from '@/stores/counter.js'
    const counter = useCounterStore() // ✅
    </script>
    ```
    |🚨 <span class="caution">CAUTION</span>|
    |:---|
    |因為我們是 `export const`，所以 import 時記得解構。<br />如果是 `export default` 就不用。|

  + `storeToRefs` 解構並保持 ref
    ```html
    <template>
      <div>
        {{ count }}
        {{ doubleCount }}
        <button @click="count++">+</button>
      </div>
    </template>

    <script setup>
    import { useCounterStore } from '@/stores/counter.js'
    import { storeToRefs } from 'pinia'
    const counter = useCounterStore()
    const { count, doubleCount } = storeToRefs(counter) // ✅
    </script>
    ```
  + `.$patch()` 打補丁 (直接更改 store 的值)
    ```html
    <template>
      <div>
        {{ count }}
        <button @click="count++">+</button>
        <button @click="updateCounter">@</button>
      </div>
    </template>

    <script setup>
    import { useCounterStore } from '@/stores/counter.js'
    import { storeToRefs } from 'pinia'
    const counter = useCounterStore()
    const { count, doubleCount } = storeToRefs(counter)

    function updateCounter() {
      counter.$patch({
        count: 9527,
      })
    }
    </script>
    ```

  + `.$reset()` 重設

    |☢️ <span class="warning">WARNING</span>|
    |:---|
    |Uncaught Error: 🍍: Store "counter" is built using the setup syntax and does not implement $reset().|
    | `$reset` 在 Composition API 中沒有定義，<br />但沒關係，我們可以自己寫一個 `$reset`。<br />(使用 VueUse 的 `useCloned()` 來 deep copy 初始值) |
    | 或者你也可以在 `defineStore()` 中順便定義 `$reset`，<br />如果 store 的內容很少、或者你的重置不是回到初始值的話。 |

    ```js
    // src/utils/resetStore.js
    import { useCloned } from '@vueuse/core'

    export default function resetStore({ store }) {
      const { cloned } = useCloned(store.$state)
      store.$reset = () => store.$patch(cloned.value)
    }
    ```
    ```js
    // src/main.js
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'

    import App from './App.vue'
    import router from './router'
    import resetStore from './utils/resetStore' // ✅ 自己寫的 plugin

    const app = createApp(App)
    const pinia = createPinia()
    pinia.use(resetStore)  // ✅ 記得註冊 plugin
    app.use(pinia)
    app.use(router)

    app.mount('#app')
    ```
    
    接下來你就能正常使用 `$reset` 了
    ```html
    <template>
      <div>
        {{ count }}
        <button @click="count++">+</button>
        <button @click="updateCounter">@</button>
        <button @click="resetCounter">!</button>
      </div>
    </template>

    <script setup>
    import { useCounterStore } from '@/stores/counter.js'
    import { storeToRefs } from 'pinia'
    const counter = useCounterStore()

    const { count, doubleCount } = storeToRefs(counter)

    function updateCounter() {
      counter.$patch({
        count: 9527,
      })
    }

    function resetCounter() {
      counter.$reset()
    }

    </script>
    ```

[Hydration Error]: https://hackmd.io/@RogelioKG/nuxt#Hydration-Error