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')
}
},
},
})