Article directory
Pinia state management
1. Pinia installation and use
Pinia (pronounced /piːnjʌ/
) is a Vue-specific state management library with a "composition API" that allows state to be shared across components or pages.
How has Pinia changed compared to Vuex 3.x/4.x?
- mutation has been deprecated, state operations are more straightforward
- Designed to provide a flat architecture, "no more nameable modules and nested structural modules"
- API design uses TS type deduction as much as possible
- No too many **"magic string" injections, just import functions and call them, and enjoy the fun of auto-completion
1.1 Installation
# yarn 方式
yarn add pinia
# npm 方式
npm install pinia
Pinia provides customized Store state management for Vite + Vue3 combined API by default. If the Vue version is lower than 2.7, please install the composition API package: @vue/composition-api. If you are using Vue CLI, you can try this unofficial plugin .
1.2 Register the pinia instance to the global
# main.js
import { createApp } from 'vue'
// 引入 createPinia 函数
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 pinia 实例(根 store)
const pinia = createPinia()
// 创建应用实例
const app = createApp(App)
// 应用中使用 pinia 插件
app.use(pinia)
// 挂载根组件
app.mount('#app')
The Vue2 version also needs to introduce the plug-in PiniaPlugin.
1.3 Create a Store
Store (eg: Pinia) is an entity that saves state and business logic, and is not bound to the component tree. It holds the global state (data accessed throughout the application), and every component that imports it can read and write to it.
There are three concepts similar to components in Store: state (data), getter (computed), and action (methods).
It should be noted that not all data must be processed in the Store, such as the shopping cart, user login status, etc., which need to quickly process the state between components, and it is more convenient to use the Store; and whether the control element is visible or not The state in the component does not require a Store.
Here is an example to describe the process of Store managing user login status:
When a user logs in, we can record the user's information in state management for use in subsequent user operations. If a user logs out on the site, the user's state can be cleared. By using Pinia, developers can easily manage the login status of users and authenticate users throughout the application.
Create a Store code example (you can skip here if you don't have the basics):
# src/stores/store-demo.js
import { defineStore } from 'pinia'
const filters = {
FINISHED: 'finished',
UNFINISHED: 'unfinished'
}
export const useTodosStore = defineStore('todos', {
state: () => ({
// @type {
{id: number, text: string,isFinished: boolean}[]}
todos: [],
// @type {'all' | 'finished' | 'unfinished'}
filter: 'all',
// 类型将自动推断为 number
id: 0
}),
getters: {
// 过滤列表中自动补全的数据
finishedTodos(state) {
return state.todos.filter(todo => todo.isFinished)
},
// 过滤列表中非自动补全的数据
unfinishedTodos(state) {
return state.todos.filter(todo => !todo.isFinished)
},
// 根据 state.filter 结果过滤数据
// @returns {
{ id: number, text: string, isFinished: Boolean }[]}
filteredTodos(state) {
console.log(this, this === state) // Proxy(Object) {$id: 'todos', $onAction: ƒ, $patch: ƒ, …} true
if (this.filter === filters.FINISHED) {
// 调用其它带有自动补全的 getters
return this.finishedTodos
} else if (this.filter === filters.UNFINISHED) {
return this.unfinishedTodos
}
return this.todos
}
},
actions: {
// 接受任何数量的参数,返回一个 Promise 或不返回
addTodo(text) {
// 状态变更
this.todos.push({ id: this.id++, text, isFinished: [true, false][parseInt(Math.random() * 2)] })
}
}
})
1.4 Using Store in components
<script setup>
import { ref } from 'vue'
import { useTodosStore } from '@/stores/index'
const todos = useTodosStore()
let todoList = ref(todos.todos)
const add = () => {
todos.addTodo('测试')
todoList.value = todos.todos
}
const getFinished = () => {
todoList.value = todos.finishedTodos
}
const getUnfinished = () => {
todoList.value = todos.unfinishedTodos
}
const getRanFilter = () => {
const ranFilter = ['all','finished','unfinished'][parseInt(Math.random() * 3)]
if (todos.filter === ranFilter) return getRanFilter()
todos.filter =ranFilter
todoList.value = todos.filteredTodos
console.log(todos.id, todos.filter,todos.todos)
}
</script>
<template>
<div class="wrap">
<button @click="add">添加 todo</button>
<button @click="getFinished">自动补全</button>
<button @click="getUnfinished">非自动补全</button>
<button @click="getRanFilter">随机过滤</button>
<ul>
<li
v-for="item in todoList"
:key="item.id">
{
{ item.text }} ~ {
{ item.id }} ~ {
{ item.isFinished ? '自动补全' : '非自动补全'}}
</li>
</ul>
</div>
</template>
<style scoped>
.wrap {
padding: 20px;
}
</style>
Through the code demonstrated above, compared to Vuex, Pinia is really delicious!
Page effect:
2. Learn the core concepts of Pinia
Definition and use of Store
The core concept to be talked about is actually the configuration information in the Store, and the Store is defined by defineStore(), which accepts two parameters:
- The first parameter is the name of the Store, which must be unique (the unique ID of the Store in the current application, Pinia will use it to connect the store and devtools)
- The second parameter is the configuration information in the Store, which covers three parts: State, Getter, Action. Accepts two types of values: Setup function or Option object
The return value after the defineStore() function is executed is recommended to be received with variables that start with use and end with store. Of course, you can also name it in any way you like.
Define Store
Configuration of the Option object: (similar to the way Vuex is written)
Similar to Vue's option API, pass in an Option object with state
, actions
and getters
attributes
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count:0 }),
getters: {
doubleCount(state){
return state.count * 2
}
},
actions: {
increment(){
this.count++
}
}
})
It can be thought of
state
as the store's data (data
),getters
as the store's computed property (computed
), andactions
as the method (methods
).In the current Store, this in non-arrow functions is equivalent to state
The way of the Setup function:
Similar to the setup function of the Vue composition API , a function can be passed in, which defines some reactive properties and methods, and returns an object with the properties and methods we want to expose.
import { ref } frrom 'vue'
export const useCounterStore = defineStore('counter', {
const count = ref(0)
function increment() {
count.value++
}
return { count, incerement }
}
In the Setup Store :
ref()
isstate
the propertycomputed()
that isgetters
function()
that isactions
Setup store brings more flexibility than Option Store , because you can create listeners inside a store and freely use any composition function . However, using composite functions makes SSR more complex. So under different rendering requirements, choose the most comfortable way to define Store.
You can define as many stores as you like, but to maximize the benefits of using pinia (such as allowing build tools to automatically do code splitting and TypeScript inference), you should define stores in separate files .
**Use Store in component**
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.increment()
console.log(counterStore.count)
</script>
<template>
<div>{
{ counterStore.doubleCount }}</div>
</template>
Once the store is instantiated, you can directly access any properties defined in the store's state
, getters
and .actions
store
is an reactive
object wrapped with a , which means you don't need to write after the getters .value
, like setup
in props
, and if you do, it can't be destructured (breaking reactivity).
In order to keep it responsive when extracting properties from the store, you can use storeToRefs()
, which will create a reference for each responsive property; while actions can be destructured directly from the store.
<script setup>
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore
</script>
2.1 State
As the core of the store, Pinia defines state as a function that returns the initial state. Can support both server and client
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// 为了完整类型推理,推荐使用箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
})
2.2 Getter
The getter is exactly equivalent to the computed value of the store's state . They can be defined via the attribute defineStore()
in getters
. Arrow functions are recommended and will receive state
as first argument:
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})
Most of the time, getters depend only on state, however, sometimes they may use other getters as well. So even when defining the getter with a regular function, we have this
access to the entire store instance via , but (in TypeScript) the return type must be defined . This is to avoid a known flaw in TypeScript, however this does not affect getters defined with arrow functions, nor this
getters that do not use < .
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注
return this.doubleCount + 1
},
},
})
Then you can directly access the getter on the store instance:
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {
{ store.doubleCount }}</p>
</template>
2.3 Action
Action is equivalent to method in component . They can be defined via attributes defineStore()
in , and they are also perfect for defining business logic.actions
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})
Similar to getters , actions can also this
access the entire store instance through , and support complete type annotations (and auto-completion ✨) . The difference is that action
they can be asynchronousawait
, and you can call any API and other actions in them ! Below is an example using Mande . Note that it doesn't matter what library you use, as long as you get one Promise
, you can even use native fetch
functions (in the browser):
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// 让表单组件显示错误
return error
}
},
},
})
You are also completely free to set any parameters you want and return any results. All types can also be automatically inferred when the action is called.
Actions can be called like functions or methods in general:
<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
<!-- 即使在模板中也可以 -->
<button @click="store.randomizeCounter()">Randomize</button>
</template>
Attached:
1. What are magic strings?
A magic string refers to a specific string or value that appears multiple times in the code and forms a strong coupling with the code.
Well-styled code should try to eliminate magic strings and replace them with variables with clear meanings.
function btnHandle (type) {
if (type === 'delete') {
// 删除操作代码
}
// 其它操作判断代码
}
btnHandle('delete')
"delete" in the above function is the magic string. Although this is an unfriendly code writing habit, it can be seen everywhere in the daily development process.
When the string is fused with the code and appears multiple times, it forms a "strong coupling" with the code, which is not conducive to future modification and maintenance of the project. So try to eliminate this kind of it, and the way to eliminate it is to define it as a variable.
const DELETE = 'delete'
function btnHandle (type) {
if (type === DELETE) {
// 删除操作代码
}
// 其它操作判断代码
}
btnHandle(DELETE)
If multiple magic strings are designed in the same operation, they can be processed by objects:
const types = {
ADD: 'add',
EDIT: 'edit',
DELETE: 'delete',
DOWNLOAD: 'download'
}
function btnHandle (type) {
seitch (type) {
case types.ADD:
// 添加操作
break;
case types.EDIT:
// 编辑操作
break;
case types.DELETE:
// 删除操作
break;
case types.DOWNLOAD:
// 下载操作
break;
}
}