[Pinia State Management] Store data processing in Vite + Vue3 combined API mode

Pinia state management

insert image description here

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, actionsand gettersattributes

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
	state: () => ({ count:0 }),
	getters: {
		doubleCount(state){
			return state.count * 2
		}
	},
	actions: {
		increment(){
			this.count++
		}
	}
})
  1. It can be thought of stateas the store's data ( data), gettersas the store's computed property ( computed), and actionsas the method ( methods).

  2. 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()is statethe property
  • computed()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, gettersand .actions

storeis an reactiveobject wrapped with a , which means you don't need to write after the getters .value, like setupin 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 stateas 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 thisaccess 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 thisgetters 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 thisaccess the entire store instance through , and support complete type annotations (and auto-completion ✨) . The difference is that actionthey 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 fetchfunctions (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;
	}
}

Guess you like

Origin blog.csdn.net/qq_39335404/article/details/131232395