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-likecomputed, 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 nomutations. 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)