# 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