Vue3$Data-Pinia
0. What is Pinia
Pinia in Big Picture
组件的数据除了可以放在自身(ref 等),也可以从外部获取(props, provide-inject, v-model, fallthrough attributes等)。上述的外部获取,通常是指父子组件或者祖孙组件。兄弟组件或者不相关的组件之间处理起来就比较麻烦。
为了方便多个组件共享数据,人们就把数据及操作数据的函数放到了一个全局单例中。Vue 之前是通过 Vuex 实现的,后来使用了 Pinia。Pinia 删除了冗余的 mutations,更加扁平化,支持组合式写法。
Pinia Core Concepts
- store:存储数据和函数的对象
- state:数据
- getters:衍生的数据
- actions:操作数据的函数
Pinia 可以使用选项式 API,也可以使用组合式 API。这里使用组合式 API。
Basic Example
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 }
})
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
</script>
<template>
<!-- Access the state directly from the store -->
<div>Current Count: {{ counter.count }}</div>
</template>
P. Install
npm install pinia之后,使用插件:
// @file main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
1. Defining a Store
import { defineStore } from 'pinia'
// the first argument is a **unique** id of the store across your application
export const useAlertsStore = defineStore('alerts', {
// other options...
})
1.1 Setup Stores
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 }
})
`ref()`s become `state` properties
`computed()`s become `getter`s
`function()`s become `action`s
Must return all state properties in setup stores: cannot have private state properties in stores.
- can use watchers
- globally provided properties (Router, Route)
- property provided at the App level
import { inject } from 'vue'
import { useRoute } from 'vue-router'
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
// this assumes `app.provide('appProvided', 'value')` was called
const appProvided = inject('appProvided')
// ...
return {
// ...
}
})
1.2 Using the store
<script setup>
import { useCounterStore } from '@/stores/counter'
// access the `store` variable anywhere in the component ✨
const store = useCounterStore()
</script>
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// ❌ This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = store
name // will always be "Eduardo"
doubleCount // will always be 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ this one will be reactive
// 💡 but you could also just use `store.doubleCount` directly
const doubleValue = computed(() => store.doubleCount)
</script>
1.3 Destructuring from a Store
store 是一个 reactive 对象,不能直接解构。
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` and `doubleCount` are reactive refs
// This will also extract refs for properties added by plugins
// but skip any action or non reactive (non ref/reactive) property
const { name, doubleCount } = storeToRefs(store)
// the increment action can just be destructured
const { increment } = store
</script>
2. State
2.1 Accessing the state
const store = useStore()
store.count++
// You cannot add a new state property.
2.2 Resetting the state
// in Option Stores
const store = useStore()
store.$reset()
// in Setup Stores: create your own `$reset()`
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
2.3 Mutating the state
// 1. direct
store.count++
// 2. patch object (partial state object)
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
// 3. patch function
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
2.4 Replacing the state
// You cannot exactly replace the state of a store as that would break reactivity.
// You can patch it:
// this doesn't actually replace `$state`
store.$state = { count: 24 }
// it internally calls `$patch()`:
store.$patch({ count: 24 })
// set the initial state of your whole application: 只在 Options API 中好用
pinia.state.value = {}
2.5 Subscribing to the state
// The advantage of using $subscribe() over a regular watch() is that subscriptions will trigger only once after patches (e.g. when using the function version from above). WHAT??? watch will trigger multiple times?
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem('cart', JSON.stringify(state))
})
<script setup>
const someStore = useSomeStore()
// this subscription will be kept even after the component is unmounted
someStore.$subscribe(callback, { detached: true })
</script>
watch(
pinia.state,
(state) => {
// persist the whole state to the local storage whenever it changes
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
3. Actions
3.1 Subscribing to actions
It is possible to observe actions and their outcome with `store.$onAction()`. The callback passed to it is executed before the action itself.
- `after` handles promises and allows you to execute a function after the action resolves.
- In a similar way, onError allows you to execute a function if the action throws or rejects.
These are useful for tracking errors at runtime, similar to this tip in the Vue docs.
const unsubscribe = someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
// a shared variable for this specific action call
const startTime = Date.now()
// this will trigger before an action on `store` is executed
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// this will trigger if the action succeeds and after it has fully run.
// it waits for any returned promised
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// this will trigger if the action throws or returns a promise that rejects
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// manually remove the listener
unsubscribe()
<script setup>
const someStore = useSomeStore()
// this subscription will be kept even after the component is unmounted
someStore.$onAction(callback, true)
</script>
4. Plugins
持久化 pinia-plugin-persistedstate
每个 store 单独开启
// store/index.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('hello pinia')
return { someState }
},
{
persist: true,
},
)
所有 store 默认开启
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(createPersistedState({
auto: true,
}))