Pinia
A new state management library for Vue
The next version of Vuex is Vuex 5.0
Pinia has been added to the official account, github.com/vuejs/pinia
Pinia website link
1. Introduction
Pinia was originally an experiment to redesign what Vue state management would look like on Composite APl, the next generation Vuex, around November 2019.
- Vuex previously served on Vue 2, an optional API
- If you want to use Vuex in Vue 3, you need to use version 4 of it
- It’s just a transitional choice, with big flaws
- So after Vue3 came along with the composite API, a new Vuex was designed: Pinia, or Vuex 5
Proposal link: github.com/vuejs/rfcs/…
-
Both Vue 2 and Vue 3 are supported
- The apis are the same except for initial installation and SSR configuration
- Official documents mainly describe Vue3 and provide comments on Vue2 when necessary
-
Support the Vue DevTools
- Track the timelines of Actions, mutations
- The container itself can be observed in the components that use it
- Support for time Travel’s easier debugging features
- In Vue 2 Pinia uses Vuex’s existing interface, so it cannot be used with Vuex
- However, debugging tool support for Vue 3 is not perfect, such as time-travel debugging
-
Module hot update
- You can modify your container without reloading the page. Maintaining any existing state while hot updating supports extension of Pinia functionality using plug-ins
-
Support for extending Pinia functionality using plug-ins
-
There is better TypeScript support than Vuex
-
Server side rendering
2. Core concepts
Pinia is almost identical to the previous Vuex in terms of usage
A Store(such as Pinia) is an entity that holds state and business logic and is not tied to your component tree. In other words **, which carries global state**. It’s kind of like an always-on component that everyone can read and write to. It has three core concepts.
state
Component-like data, used to store global state
{
todos: [{id: 1Title:'eat', done: fa1se}, {id: 1Title:'eat'.done: true },
{ id: 1Title:'eat'.done: false}}]Copy the code
getters
- Getters: component-like
computed
, encapsulates derived data based on existing state, and also has caching features
doneCount() {
return todos.filter(item= > item.done).length
}
Copy the code
actions
- Actions: component-like methods used to encapsulate business logic, synchronous or asynchronous
- VueX requires simultaneous use of mutations and asynchronous use of Actions
Note: Pinia is not mutations
3. Basic examples
// store/counter.js
import { defineStore } from "pinia";
// defineStore returns a function called to get the Store entity
export const useCounterStore = defineStore("counter", {// state: a function that returns an object
state: () = > {
return { count: 0}},actions: {
increment() {
2. define mutations 2. Submit mutations
this.count++
}
}
});
Copy the code
Used in components
<template> <div>{{store.count}}</div> </template> <script> Use your own path import {useCounterStore} from "@/store/counter"; Export default {setup() {// Call the function to get Store const counter = useCounterStore(); counter.count++; // With autocompletion ✨ counter.$patch({count: counter.count + 1 }) // or using an action instead counter.increment(); return { counter } } } </script>Copy the code
You can even define stores for more advanced use cases using functions (like component setup()) :
export const useCounterStore = defineStore('counter'.() = > {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
Copy the code
If you’re not familiar with the Setup ()Composition API, Pania also supports Map Helpers like Vuex. You define stores in the same way, using mapStores(), mapState(), or mapActions() :
const useCounterStore = defineStore('counter', {
state: () = > ({ count: 0 }),
getters: {
double: (state) = > state.count * 2,},actions: {
increment() {
this.count++
}
}
})
const useUserStore = defineStore('user', {
// ...
})
export default {
computed: {
// other computed properties
// ...
// gives access to this.counterStore and this.userStore. mapStores(useCounterStore, useUserStore)// gives read access to this.count and this.double. mapState(useCounterStore, ['count'.'double']),},methods: {
// gives access to this.increment(). mapActions(useCounterStore, ['increment']),}},Copy the code
4.Pinia vs Vuex
The Pinia API is very different from Vuex≤4, namely:
- There is no
mutations
. Mutations are considered very lengthy. Devtools integration was initially brought in, but this is no longer an issue. - There is no more nested structure of modules. You can still implicitly nest stores by importing and using stores in another store, but Pinia provides a flat structure by design while still supporting cross-store composition. You can even have cyclic store dependencies.
- Better typescript support. There’s no need to create complex custom wrappers to support TypeScript, everything is typed, and the API is designed to take advantage of TS type inference as much as possible.
- No more need to inject, import functions, call them, and enjoy autocomplete!
- There is no need to add stores dynamically; by default they are all dynamic and you won’t even notice. Note that you can still register it manually with store at any time, but because it’s automatic, you don’t have to worry.
- No namespace module. Given the flat architecture of stores, “namespace” stores are inherent in the way they are defined, so you could say that all stores are namespace.
Pinia is a better Vuex, and it is recommended that you use it directly in your projects, especially those that use TypeScript.
5. Quick start
Installed 5.1.
Installation requires @next because Pinia 2 is in beta and Pinia 2 is the version corresponding to Vue3
yarn add pinia
# or with npm
npm install pinia
Copy the code
5.2. Perform initial Configuration
Create a pinia (root storage) and pass it to the application:
import { createPinia } from 'pinia'
app.use(createPinia())
Copy the code
5.3. Definition of the State
Create a SRC/store/index. Ts
import { defineStore } from 'pinia'
// Parameter 1: the container ID, which must be unique, Pinia will mount all containers to the same container in the future
// Parameter 2: option object
// Return value: a function called to get the instance of the container
export const useMainStore = defineStore("main", {
// id: 'main', // id can also be defined here
// Similar to the data component, used to store global state
// 1. Must be a function: to avoid cross-request data state contamination during server rendering
// 2. Must be arrow function, for better TS type derivation
state: () = > {
return {
count: 100.foo: "bar".arr: [1.2.3]}}})Copy the code
5.4. Access to the state
<template>
<div>{{ mainStore.count }}</div>
</template>
<script lang="ts" setup>
import { useMainStore } from '@/store'
const mainStore = useMainStore()
</script>
Copy the code
Combined computed acquisition
const count = computed(() = > mainStore.count)
Copy the code
State can also use deconstruction, but using deconstruction makes it unresponsive, so Pinia’s storeToRefs can be used instead.
import { storeToRefs } from 'pinia'
const { count } = storeToRefs(mainStore)
const { count } = mainStore // The data response will be lost
Copy the code
5.5. Modify the state
Method 1: the simplest way
mainStore.count++;
mainStore.foo = "yunmu"
Copy the code
Method 2: If multiple data needs to be modified, you are advised to update $patch in batches
mainStore.$patch({
count: mainStore.count + 1.foo: "yunmu".arr: [...mainStore.arr, 4]})Copy the code
Way 3: better batch update way $patch a function
mainStore.$patch(state= > {
state.count++
state.foo = "yunmu"
state.arr.push(4)})Copy the code
Way four: more logic can be encapsulated to actions processing
mainStore.changeState(10)
Copy the code
import { defineStore } from 'pinia'
export const useMainStore = defineStore('main', {state: () = > {
return {
count: 100.foo: "bar".arr: [1.2.3]}},// Similar component mthods, encapsulate business logic, modify state
actions: {
// Do not use the arrow function to modify the action. This will cause the this pointer to be lost because the arrow function binds to the external this
changeState(num: number) {
// Use this to access the data in state
this.count += num
this.foo = "yunmu"
this.arrr.push(4)
/ / can also use this $patch ({}) or enclosing $patch (state = > {})}}})Copy the code
5.6. Getters
import { defineStore } from 'pinia'
import { otherState } from "@/store/otherState.js";
export const useMainStore = defineStore('main', {state: () = > {
return {
count: 100.foo: "bar".arr: [1.2.3]}},// Analogous component computed, used to encapsulate computed attributes, has a caching function
gettters: {
// The function takes an optional argument, the state object
countPlus10(state) {
console.log('countPlus called ')
return state.count + 10
}
// If getters uses this and does not accept state, the type of return value must be manually specified otherwise it cannot be derived
countPlus20(): number{
return this.count + 10
}
// Get other getters, directly through this
countOtherPlus() {
return this.countPlus20;
}
// Use other stores
otherStoreCount(state) {
// Here are other stores, called to get Store, just like in setup
const otherStore = useOtherStore();
returnotherStore.count; }}})Copy the code
Components use
mainStore.countPlus10
Copy the code
5.7. Asynchronous action
Actions support async/await syntax and can easily handle asynchronous processing scenarios.
export const useUserStore = defineStore('user', {
actions: {
async login(account, pwd) {
const { data } = await api.login(account, pwd)
return data
}
}
})
Copy the code
5.8. Action calls each other
Calls between actions can be accessed using this.
export const useUserStore = defineStore('user', {
actions: {
async login(account, pwd) {
const { data } = await api.login(account, pwd)
this.sendData(data) // Call another action method
return data
},
sendData(data) {
console.log(data)
}
}
})
Copy the code
It is also easy to call an action from another store. After importing the corresponding store, you can access its internal methods.
// src/store/user.ts
import { useAppStore } from './app'
export const useUserStore = defineStore('user', {
actions: {
async login(account, pwd) {
const { data } = await api.login(account, pwd)
const appStore = useAppStore()
appStore.setData(data) // Call the app Store action method
return data
}
}
})
Copy the code
5.9. Data persistence
The plug-in pinia-plugin-persist assists in data persistence.
1. Install
npm i pinia-plugin-persist
Copy the code
2. Use
// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const store = createPinia()
store.use(piniaPluginPersist)
export default store
Copy the code
Simply enable Persist in the corresponding store
export const useUserStore = defineStore('user', {
// Enable data cache. Data is stored in sessionStorage by default, and store ID is used as key.
persist: {
enabled: true
},
state: () = > {
return {
name: 'yunmu'}}})Copy the code
3. Customize keys
- You can also customize key values in strategies and change the storage location from sessionStorage to localStorage.
persist: {
enabled: true.strategies: [{key: 'userInfo'.storage: localStorage,}}]Copy the code
4. Persist state
- By default, all states are cached, so you can specify fields to persist through Paths and not others.
state: () = > {
return {
name: 'yunmu'.age: 18.gender: 'male'}},// Persist only name and age to localStorage
persist: {
enabled: true.strategies: [{storage: localStorage.paths: ['name'.'age']]}}Copy the code
6.Pinia case study
1. Requirement description
-
List of goods
- Show a list of goods
- Add to shopping cart
-
The shopping cart
- Display the list of items in the shopping cart
- Show total price
- Order and settlement
- Show settlement status
2. Create a startup project
npm init vite@latest Need to install the following packages: create-vite@1atest ok to proceed? (y) √ Project name:... Shopping - Cart √ select a framework: > Vue √ select a variant: > vue-ts scaffo1ding project in c:\Users\yun\Projects\pinia-examp1es\shopping-cart. . . Done. Now run: cd shopping-cart npm insta11 npm run devCopy the code
3. Page template
<! -- SRC/app. vue --> <template> <div> <h1>Pinia -- ShoppingCart example </h1> <hr /> <h2> <ProductList /> <hr /> <ShoppingCart /> </div> </template> <script setup lang="ts"> import ProductList from "./components/ProductList.vue"; import ShoppingCart from "./components/ShoppingCart.vue"; </script> <style lang="scss" scoped></style>Copy the code
<!-- src/ProductList.vue -->
<template>
<ul>
<li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
<li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
<li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
</ul>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>
Copy the code
<! -- SRC/shoppingcart. vue --> <template> <div class="cart"> <h2> Please add some items to your cart </ I ></p> <ul> <li> Item name - item price × Commodity quantity </li> <li> Commodity name - Commodity price × commodity quantity </li> <li> XXX < / p > < p > < button > settlement < / button > < / p > < p > settlement success/failure < / p > < / div > < / template > < script setup lang = "ts" > < / script > < style lang="scss" scoped></style>Copy the code
4. Data interface
// src/api/shop.ts
export interface IProduct {
id: number;
title: string;
price: number;
inventory: number;
}
const _products: IProduct[] = [
{ id: 1.title: "Apple 12".price: 600.inventory: 3 },
{ id: 2.title: "Millet 13".price: 300.inventory: 5 },
{ id: 3.title: "The meizu 12".price: 200.inventory: 6},];// Get the list of items
export const getProducts = async() = > {await wait(100);
return _products;
};
// Settle goods
export const buyProducts = async() = > {await wait(100);
return Math.random() > 0.5;
};
async function wait(delay: number) {
return new Promise((resolve) = > setTimeout(resolve, delay));
}
Copy the code
5. Display the product list
// src/store/products.ts
import { defineStore } from "pinia";
import { getProducts, IProduct } from ".. /api/shop";
export const useProductsStore = defineStore("products", {
state: () = > {
return {
all: [] as IProduct[], // List of all items
};
},
getters: {},
actions: {
async loadAllProducts() {
const result = await getProducts();
this.all = result; ,}}});Copy the code
<! -- ProductList.vue --> <template> <ul> <li v-for="item in productsStore.all" :key="item.id"> {{ item.title }} - {{ Item. price}}¥-inventory {{item.inventory}}<br /> <button> Add to shopping cart </button> </li> </ul> </template> <script setup lang="ts"> import { useProductsStore } from ".. /store/products"; const productsStore = useProductsStore(); / / load all data productsStore. LoadAllProducts (); </script> <style lang="scss" scoped></style>Copy the code
6. Add to shopping cart
// src/store/cart.ts
import { defineStore } from "pinia";
import { IProduct, buyProducts } from ".. /api/shop";
import { useProductsStore } from "./products";
// Add quantity type and merge IProduct except inventory, final data {id, title, price, quantity}
type CartProduct = {
quantity: number;
} & Omit<IProduct, "inventory">;
export const useCartStore = defineStore("cart", {
state: () = > {
return {
cartProducts: [] as CartProduct[], // Shopping cart list
};
},
getters: {},
actions: {
addProductToCart(product: IProduct) {
console.log("addProductToCart", product);
// Check if the goods are in stock
if (product.inventory < 1) {
return;
}
// Check if the cart already has the item
const cartItem = this.cartProducts.find((item) = > item.id === product.id);
if (cartItem) {
// If yes, the quantity of goods + 1
cartItem.quantity++;
} else {
// If not, add to shopping cart list
this.cartProducts.push({
id: product.id,
title: product.title,
price: product.price,
quantity: 1.// The first time it is added to the cart is 1
});
}
// Update the inventory to another store
// product.inventory--; Do not do this, do not trust function arguments, recommend to find the source data to modify
constproductsStore = useProductsStore(); productsStore.decrementProduct(product); ,}}});Copy the code
// src/store/products.ts
actions: {
async loadAllProducts() {
const result = await getProducts();
this.all = result;
},
// Reduce inventory
decrementProduct(product: IProduct) {
const result = this.all.find((item) = > item.id === product.id);
if(result) { result.inventory--; }}},Copy the code
<! -- ProductList.vue --> <template> <ul> <li v-for="item in productsStore.all" :key="item.id"> {{ item.title }} - {{ Item. Price}} - RMB inventory {{item. The inventory}} < br / > < button @ click = "cartStore. AddProductToCart (item)" : disabled = "! Item. inventory"> Add to shopping cart </button> </li> </ul> </template> <script setup lang="ts"> import {useProductsStore} from ".. /store/products"; import { useCartStore } from ".. /store/cart"; const productsStore = useProductsStore(); const cartStore = useCartStore(); / / load all data productsStore. LoadAllProducts (); </script> <style lang="scss" scoped></style>Copy the code
<! -- ShoppingCart. Vue --> <template> <div class="cart"> <h2> Please add some items to your cart </ I ></p> <ul> <li V -for="item in Cartstore. cartProducts" :key="item.id"> {{item.title}} - {{item.price}}¥× {{item.quantity}} </li> </ul> < P > Total commodity price: XXX </p> <p><button> </p> <p>< /p> <p>< /p> </p> </p> </p> </p> </p> </p> <script setup lang="ts"> import {useCartStore} from ".. /store/cart"; const cartStore = useCartStore(); </script> <style lang="scss" scoped></style>Copy the code
7. Show the total shopping cart price
// src/store/cart.ts
getters: {
/ / the total price
totalPrice(state) {
return state.cartProducts.reduce((total, item) = > {
return total + item.price * item.quantity;
}, 0); }},Copy the code
<! Cart. Vue --> <p> Cart price: {{cartStore. TotalPrice}}</p>Copy the code
8. Shopping cart case completed
// src/store/cart.ts
import { defineStore } from "pinia";
import { IProduct, buyProducts } from ".. /api/shop";
import { useProductsStore } from "./products";
// Add quantity type and merge IProduct except inventory, final data {id, title, price, quantity}
type CartProduct = {
quantity: number;
} & Omit<IProduct, "inventory">;
export const useCartStore = defineStore("cart", {
state: () = > {
return {
cartProducts: [] as CartProduct[], // Shopping cart list
checkutStatus: null as null | string, // Clearing status
};
},
getters: {
/ / the total price
totalPrice(state) {
return state.cartProducts.reduce((total, item) = > {
return total + item.price * item.quantity;
}, 0); }},actions: {
addProductToCart(product: IProduct) {
console.log("addProductToCart", product);
// Check if the goods are in stock
if (product.inventory < 1) {
return;
}
// Check if the cart already has the item
const cartItem = this.cartProducts.find((item) = > item.id === product.id);
if (cartItem) {
// If yes, the quantity of goods + 1
cartItem.quantity++;
} else {
// If not, add to shopping cart list
this.cartProducts.push({
id: product.id,
title: product.title,
price: product.price,
quantity: 1.// The first time it is added to the cart is 1
});
}
// Update the inventory to another store
// product.inventory--;
const productsStore = useProductsStore();
productsStore.decrementProduct(product);
},
async checkout() {
const result = await buyProducts();
this.checkutStatus = result ? "Success" : "Failure";
// Empty the shopping cart
if (result) {
this.cartProducts = []; ,}}}});Copy the code
<! <p>< p v-show=" cartstore. checkutStatus"> < {cart -> <p>< p v-show=" cartstore. checkutStatus"> cartStore.checkutStatus }}</p>Copy the code
Thanks for watching, part of this article is from: a new generation of status management tool, Pinia. Js guide – Digging gold (juejin. Cn)