Vue3——Chapter Fourteen (Pinia: Vue Store Worth Your Love)

1. Introduction to Pinia

  • Pinia is a Vue-specific state management library that allows you to share state across components or pages.
  • Pinia originated as an experiment to explore the next iteration of Vuex, and as such incorporates many ideas from the Vuex 5 core team discussions.
  • mutation has been deprecated. They are often considered extremely redundant. Their original intention was to bring devtools integration solutions, but this is no longer an issue.
  • Vuex 3.x is only compatible with Vue 2, while Vuex 4.x is compatible with Vue 3.
  • Extremely lightweight: Pinia is only about 1kb in size
  • Development tool support: Whether it is Vue 2 or Vue 3, Pinia that supports Vue devtools hooks can give you a better development experience.
  • Type Safety: Types are automatically inferred and autocompletion is provided for you even in JavaScript!
  • Pinia started around November 2019 as an experiment to design a Vue state management library with a compositional API.
  • Since then, there has been a tendency to support both Vue 2 and Vue 3, and developers are not forced to use the composition API.

2. Basic example

  • The following is the basic usage of pinia API
  • You can create a Store first:
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // can also be defined like this
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})
  • For more advanced usage, you can even use a function (similar to component setup() ) to define a Store:
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})
  • Then you can use the store in a component:
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()

    counter.count++
    // With autocompletion ✨
    counter.$patch({ count: counter.count + 1 })
    // or use action instead
    counter.increment()
  },
}
  • If you're not familiar with the setup() function and the composition API, Pinia also provides a set of helper functions similar to Vuex's mapping state, accessed through mapStores(), mapState() or mapActions():
export default {
  computed: {
    // other computed properties
    // ...
    // Allow access to this.counterStore and this.userStore
    ...mapStores(useCounterStore, useUserStore)
    // Allow reading this.count and this.double
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // Allow reading this.increment()
    ...mapActions(useCounterStore, ['increment']),
  },
}
  • In non-component js files use:
import { useCounterStore } from '@/stores/counter'

function xxxx = () =>{
   const counter = useCounterStore()
   counter.count++
   counter.increment()
}

3. Core concepts

1. Define Store
  • We need to know that Store is defined with defineStore(), and its first parameter requires a unique name:
import { defineStore } from 'pinia'

// You can name the return value of `defineStore()` whatever you want
// But it's better to use the name of the store, starting with `use` and ending with `Store`. (e.g. `useUserStore`, `useCartStore`, `useProductStore`)
// The first parameter is the unique ID of the Store in your app.
export const useStore = defineStore('main', {
  // Other configurations...
})
  • This name, also used as id , is mandatory and Pinia will use it to connect the store with devtools.
  • The second parameter to defineStore() can accept two types of values: a Setup function or an Option object.
  • Similar to Vue's options API, we can also pass in an Option object with state, actions and getters properties:
// The state can be considered as the data of the store (data)
//getters are computed properties of the store
//And actions are methods (methods)
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
  • Similar to the setup function of the Vue composition API, we can pass in a function that defines some reactive properties and methods, and returns an object with the properties and methods we want to expose.
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})
  • In the Setup Store: ref() is the state property, computed() is the getters, function() is the actions
  • Setup store brings more flexibility than Option Store, because you can create listeners inside a store and use any composition function freely.
2,State
  • In most cases, state is the heart of your store.
  • People usually start by defining the state that represents their APP.
  • In Pinia, state is defined as a function that returns the initial state. This allows Pinia to support both server and client.
import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // For complete type inference, arrow functions are recommended
  state: () => {
    return {
      // All these properties will have their types automatically inferred
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})
  • By default, you can access state through the store instance, reading and writing directly to it.
const store = useStore()

store.count++
  • The state can be reset to its initial value by calling the store's $reset() method.
const store = useStore()

store.$reset()
  • Instead of directly mutating the store with store.count++, you can also call the $patch method. It allows you to change multiple properties at the same time with a single state patch object:
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})
  • The $patch method also accepts a function to combine such changes that are difficult to achieve with patch objects.
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})
  • You can't completely replace the store's state, because that would break its responsiveness. However, you can patch it.
// This doesn't actually replace `$state`
store.$state = { count: 24 }
// Call `$patch()` inside it:
store.$patch({ count: 24 })
3,Getter
  • The Getter is exactly equivalent to the computed value of the store's state.
  • They can be defined via the getters property in defineStore(). Arrow functions are recommended and will receive state as the 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 we can access the entire store instance through this even when defining the getter with a regular function
export const useStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // automatically deduces that the return type is a number
    doubleCount(state) {
      return state.count * 2
    },
    // The return type **MUST** be set explicitly
    doublePlusOne(): number {
      // Auto-completion and type annotations for the entire store ✨
      return this.doubleCount + 1
    },
  },
})
  • Then you can directly access the getter on the store instance:
<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

<script>
export default {
  setup() {
    const store = useStore()

    return { store }
  },
}
</script>
4,Action
  • Action is equivalent to method in component.
  • They can be defined via the actions attribute in defineStore() and they are also perfect for defining business logic.
export const useStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})
  • Similar to getter s, action s can also access the entire store instance through this, and support full type annotations (and auto-completion ✨).
  • The difference is that actions can be asynchronous, you can await in them to call any API, and other actions!
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)
        // Make form components display errors
        return error
      }
    },
  },
})
  • Action s can be called like methods:
export default defineComponent({
  setup() {
    const main = useMainStore()
    // Call the action as a method of the store
    main.randomizeCounter()

    return {}
  },
})
  • If you want to use another store, you can call it directly in the action:
import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

Tags: Javascript Front-end Vue Vue.js

Posted by zeberdeee on Sat, 14 Jan 2023 16:08:32 +0300