I met

In the Vue Ecological Conference on October 23, the frequency of the word “Pinia” appeared in Q&A was quite high. After almost every teacher shared it, there would be some questions related to “Pinia” in the bullet screen. Who is it? This article will take you through it.

Before we get to know Pinia, let’s take a look at an RFC for Vuex 5.x on Vue:

As you can see from the description, Vue 5.x mainly improves the following features:

  • At the same time supportcomposition apioptions apiGrammar;
  • To get rid ofmutations, onlystate,gettersactions;
  • Nested modules are not supported by compositionstoreTo take the place of;
  • More perfectTypescriptSupport;
  • Clear, explicit code splitting;

Pinia is a plaything based on RFC.

Its positioning and characteristics are also clear:

  • Intuitive, defined like a componentstoreAnd be able to combine them better;
  • The completeTypescriptSupport;
  • associatedVue DevtoolsHooks to provide a better development experience;
  • Modular design, able to build multiplestoresAnd realize automatic code splitting;
  • It is so light (1KB) that it is not even felt;
  • Both synchronous and asynchronous are supportedactions;

So niubility, next I will show you the code to learn one by one.

contact

All talk and no action is worth learning. This section introduces Pinia’s API to get a feel for the features described in the previous section.

Use Vite to quickly create a vue template project:

yarn create @vitejs/app pinia-learning --template vue-ts

cd pinia-learning
yarn
yarn dev
Copy the code

After the project is up and running, install Pinia and initialize a store:

yarn add pinia
Copy the code

Define a reference to pinia under SRC /main.ts:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

createApp(App).use(createPinia()).mount('#app')
Copy the code

Understand (State)

defineStore

Now that we can define our store and use it in the component, we create a new SRC /store/index.ts file and define a store:

import { defineStore } from 'pinia'

export default defineStore({
  id: 'app',

  state () {
    return {
      name: 'Code nong Xiao Yu'}}})Copy the code

To use the store, import the above files in app.vue:

<template>
  <div>{{ store.name }}</div>
</template>

<script setup lang="ts">
import useAppStore from './store/index'
const store = useAppStore()
console.log(store)
</script>
Copy the code

DefineComponent <==> defineStore, id <==> name, state <==> setup, defineStore as component. In the options API, we use the same method as Vuex. We use the mapState or mapWritableState helper to read and write state:

<template>
  <div>{{ this.username }}</div>
  <div>{{ this.interests.join(',') }}</div>
</template>

<script lang="ts">
import { mapState, mapWritableState } from 'pinia'
import infoStore from '.. /store/info'

export default {
  name: 'HelloWorld'.computed: {
    // Read-only computing properties. mapState(infoStore, ['interests']),
    
    // Read and write compute properties. mapWritableState(infoStore, {username: 'name'
    })
  },

  mounted () {
    this.interests.splice(1.1.'football')
    this.username = 'Jouryjc'}}</script>
Copy the code

storeToRefs

So what does the second sentence mean? And be able to combine them better. For example, on November 11, Yu, of course, had to stuff a few copies of “Sunflower Book” into her cart, and thus needed a Cart Store:

import { defineStore } from 'pinia'

export default defineStore('cart', {
  state () {
    return {
      books: [{name: 'Golden Bottle plum'.price: 50
        },
        {
          name: 'Microservices Architecture Design Patterns'.price: 139
        },
        {
          name: 'Data Intensive Application System Design'.price: 128}}}})Copy the code

Then combine cartStore in AppStore:

// store/index.ts
import { defineStore } from 'pinia'
import useCartStore from './cart'

export default defineStore('app', {
  state () {
    // Use cartStore directly
    const cartStore = useCartStore()

    return {
      name: 'Code nong Xiao Yu'.books: cartStore.books
    }
  }
})
Copy the code

It is eventually consumed by the App component. ⚠️ Destructing the store directly makes it unresponsive. To extract properties from the store while maintaining its responsiveness, use storeToRefs, as shown in the following code:

<template>
  <div>{{ name }}</div>
  <p>Shopping cart List:</p>
  <p v-for="book of books" :key="book.name">{{book.name}} price {{book.price}}</p>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import useAppStore from './store/index'

// ⚠️⚠️ returns a reactive object that cannot be destructed directly, using the storeToRefs API provided by Pinia
const { name, books } = storeToRefs(useAppStore())
</script>
Copy the code

The page will look like the following:

$patch

In addition to directly changing the value of store. XXX, you can also modify multiple fields through $patch. Add the purchase quantity, total price, and payer in the example below:

<template>
    <div>{{ name }}</div>
    <p>Shopping cart List:</p>
    <p v-for="book of books" :key="book.name">Price: {{book.price}} Quantity: {{book.count}}<button @click="add(book)">+</button>
        <button @click="minus(book)">-</button>
        </p>
    <button @click="batchAdd">Add all of them to 10</button>
    <p>Total price: {{price}}</p>
    <button @click="reset()">reset</button>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import useAppStore from "./store/index";
import type { BookItem } from "./store/cart";

const store = useAppStore();
const { name, books, price } = storeToRefs(store);

const reset = () = > {
  store.$reset();
};

const add = (book: BookItem) = > {
  // Modify store.book directly
  book.count++;
};

const minus = (book: BookItem) = > {
  // Modify store.book directly
  book.count--;
};

const batchAdd = () = > {
  // Use the $patch method to modify multiple fields in store
  store.$patch({
    name: 'the little I'.books: [{name: "Golden Vase plum".price: 50.count: 10}, {name: "Microservices Architecture Design Patterns".price: 139.count: 10}, {name: "Design of Data-intensive Applications".price: 128.count: 10,}]}); };</script>
Copy the code

The “Add all to 10” and “reset” buttons are added. Clicking on the “add all to 10” button will add the total number of books to 10, clicking on the “reset” button will reset to 0.

If you only want to change the number of microservices Architecture Design Patterns to 10, the $patch method requires this:

<template>
	<button @click="batchAddMicroService">Micro services increased to 10 books</button>
</template>

<script>
	const batchAddMicroService = () = > {
      store.$patch({
        name: 'the little I'.books: [{name: "Golden Vase plum".price: 50.count: 0}, {name: "Microservices Architecture Design Patterns".price: 139.count: 10}, {name: "Design of Data-intensive Applications".price: 128.count: 0,}]}); }</script>
Copy the code

As you can see, even if you only modify the second item in the array (collection), you still need to pass in the entire books array, resulting in the function as the $patch argument:

<script>
const batchAddMicroService = () = > {
  store.$patch((state) = > {
    state.books[1].count = 10;
  });
}
</script>
Copy the code

The above code overrides the batchAddMicroService method.

$subscribe

This method, similar to VUex’s SUBSCRIBE, listens for state and mutation actions. In the example above, we subscribe to the appStore:

const store = useAppStore();

store.$subscribe((mutation, state) = > {
  console.log(mutation);
  console.log(state);
});
Copy the code

When we add a copy of Jin Ping Mei, $SUBSCRIBE will be triggered, and the log result is as follows:

Getters

Getters is a computed property of the store. Most of the time, getters do their calculations with the state value, in which case TypeScript can correctly infer the type. Such as:

export default defineStore('app', {
  state: () = > {
    const userInfoStore = useUserInfoStore()
    const cartStore = useCartStore()

    return {
      name: userInfoStore.name,
      books: cartStore.books
    }
  },

  getters: {
    price: (state) = > {
      return state.books.reduce((init: number, curValue: BookItem) = > {
        return init += curValue.price * curValue.count
      }, 0)}}})Copy the code

We change both state and getters to arrow functions so that we can correctly infer the type of price in app.vue. Let’s verify:

Bingo! If we use this to access state in getters, we need to explicitly declare the return value to mark the type correctly. Let’s try:

export default defineStore('app', {
  // ...
  getters: {
    price () {
      return this.books.reduce((init: number, curValue: BookItem) = > {
        return init += curValue.price * curValue.count
      }, 0)}}})Copy the code

The results are as follows:

We show price the declaration return type:

export default defineStore('app', {
  // ...
  getters: {
    price (): number {
      return this.books.reduce((init: number, curValue: BookItem) = > {
        return init += curValue.price * curValue.count
      }, 0)}}})Copy the code

The type of price is now correctly prompted.

Other uses of Getters, such as combining Getters, using them in the SETUP or options API, passing arguments, and so on, are similar to State and won’t be covered in detail in this section.

Actions

Actions is equivalent to methods in a component. Of course, when buying things on Double 11, discount is inevitable. The merchant also designed an activity in the discount link, allowing the customer to set a random discount rate. Then, he defined Changedisate method under actions in store:

export default defineStore('app', {
  state: () = > {
    const userInfoStore = useUserInfoStore()
    const cartStore = useCartStore()
    const discountRate = 1

    return {
      name: userInfoStore.name,
      books: cartStore.books,
      discountRate
    }
  },

  actions: {
    changeDiscountRate () {
      this.discountRate = Math.random() * this.discountRate
    }
  }
})
Copy the code

Just like Getters, this is used in actions to retrieve the entire store. We use asynchronous actions to make the modification discount have a delay effect:

function getNewDiscountRate (rate: number) :Promise<number> {
  return new Promise ((resolve) = > {
    setTimeout(() = > {
      resolve(rate * Math.random())
    }, 1000)})}export default defineStore('app', {
  // ...
  actions: {
    async changeDiscountRate () {
      this.discountRate = await getNewDiscountRate(this.discountRate)
    }
  }
})
Copy the code

$onAction

The $onAction subscriber is handy when we want to count the time of actions or the total number of discount clicks. Here is an official example:

// App.vue
const unsubscribe = store.$onAction(
	({ name, store, args, after, onError, }) = > {
    const startTime = Date.now()
    console.log(`Start "${name}" with params [${args.join(', ')}]. `)

    after((result) = > {
      console.log(
        `Finished "${name}" after The ${Date.now() - startTime
        }ms.\nResult: ${result}. `
      )
    })

    onError((error) = > {
      console.warn(
        `Failed "${name}" after The ${Date.now() - startTime}ms.\nError: ${error}. `)})})function getNewDiscountRate (rate: number) :Promise<number> {
  return new Promise ((resolve, reject) = > {
    setTimeout(() = > {
      // Terminate the promise with reject
      reject(rate * Math.random())
    }, 1000)})}export default defineStore('app', {
  // ...

  actions: {
    async changeDiscountRate () {
      try {
        this.discountRate = await getNewDiscountRate(this.discountRate)
      } catch (e) {
        // The example performs this part of the logic
        throw Error(e)
      }
    }
  }
})
Copy the code

$onAction will execute to the onError hook, which is linked to vue3’s errorHandler. $onAction will execute to the onError hook, which is linked to vue3’s errorHandler. The next article will look at the pinia source code. The final execution effect is shown in the figure below:

As you can see from the figure above, the first WARN is returned from the subscriber $onAction, and the second warn is returned from errorHandler.

Finally, $onAction is typically created under component setup and is automatically cancelled when the component is unmounted. If you don’t want it to unsubscribe, you can set the second parameter to true:

store.$onAction(callback, true)
Copy the code

In-depth (Plugins)

With some low-level apis, we can make a variety of extensions:

  • tostoreAdd new attributes;
  • tostoreAdd new options;
  • tostoreAdd new methods;
  • Existing methods of packaging;
  • Modify or deleteactions;
  • Specific basedstoreDo extension;

All talk and no action is worth learning. To implement a pinia-plugin, first create pinia-plugin.ts under store:

import { PiniaPluginContext } from 'pinia'

export default function myPiniaPlugin(context: PiniaPluginContext) {
  console.log(context)
}
Copy the code

Then introduce the plugin in main.ts:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPlugin from './store/pinia-plugin'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPlugin)

createApp(App).use(pinia).mount('#app')
Copy the code

The context printed by the appStore looks like this:

From context we can get the app instance created from createApp(), the configuration in defineStore, the Pinia instance created from createPinia(), and the current Store object. With this information, you can do all of the above extensions just by playing with the context. The following columns illustrate two of the more common uses.

Add new properties to store

We implement a plug-in that adds state:

import { PiniaPluginContext } from 'pinia'

export default function myPiniaPlugin(context: PiniaPluginContext) {
  const { store } = context

  store.pluginVar = 'Hello store pluginVar'

  // Can be used in devTools
  store.$state.pluginVar = 'Hello store $state pluginVar'

  if (process.env.NODE_ENV === 'development') {
    // Add custom attributes that can be captured by DevTool
    store._customProperties.add('pluginVar')}setTimeout(() = > {
    store.pluginVar = 'Hello store pluginVar2'
    store.$state.pluginVar = 'Hello store $state pluginVar2'
  }, 1000)}Copy the code

Changes to store.pluginvar in the code above are not heard by DevTools, However, changes to store._customProperties. Add (‘pluginVar’) store.$state.pluginvar will be heard by DevTools.

Extend based on a specific store

Action support async, we need to send requests in action more, we can add axios as an extension to store:

import { PiniaPluginContext } from 'pinia'
import { markRaw } from 'vue'
import axios from 'axios'

export default function myPiniaPlugin(context: PiniaPluginContext) {
  const { store } = context

  store.axios = markRaw(axios)
}
Copy the code

When called store.testAxios, we just need to get the Axios object directly through this:

import { defineStore } from 'pinia'
import useCartStore, { BookItem } from './cart'
import useUserInfoStore from './info'

function getNewDiscountRate (rate: number) :Promise<number> {
  return new Promise ((resolve, reject) = > {
    setTimeout(() = > {
      reject(rate * Math.random())
    }, 1000)})}export default defineStore('app', {
  // ...
  actions: {
    testAxios () {
      // this.testAxios.$get().then().catch(() => {})
      console.log(this.axios)
    }
  }
})
Copy the code

Pinia or Vuex

Pinia is a rising star of Vue state management and Vuex maintained by the official team. When should Pinia be used? This section is a brief comparison.

First downloads and community:

Pinia, as an upstart, is certainly not as good a solution on Stack Overflow as Vuex, recommended by the Vue core team, in terms of downloads. Other comparisons can be made directly by reading Pinia-VS-Vuex, which compares the two in great detail. Bottom line: Small projects can test the waters with Pinia, big projects still use the mature Vuex. (In addition, the comparison time of the outer chain is earlier, Pinia seems to support the time travel function at present)

conclusion

This article covers the basics of Pinia in detail, from state, Getters, Actions to Plugins, with examples to help you learn. Pinia features are also covered, such as composable Store, TypeScript support, and the integration of the DevTool plugin shown below:

Finally, I made a brief comparison with Vuex. Please click the link to check the citations. By the end of this article, you should have no problem with basic Pinia usage. In the next article, let’s take a deep understanding of Pinia’s features from the perspective of source code. Pay attention to me and reap the highlights for the first time.