# 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