Using Pinia


Pinia is state management library that allows developers to define objects and methods that are accessible to all of the components in an application.

Installation

When you created your Vue project using npm create vue@latest you instructed Vue to install the pinia plugin.  To use pinia in your app you need to load the plugin into the app by adding the following bold code to your src/main.js file if it is not already there.

[code language=”JavaScript”]
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')
[/code]

Defining Stores

In pinia, a store is a related collection of state information and methods.  With pinia we can create multiple stores in an application and within an individual component use only the stores that we need.

Vue created a src/stores directory when you created your project.  Inside this directory you can create pinia store definitions.  Each store definition resides in a separate .js file and the file’s name should reflect the type of state information that is stored in the store.  For example if the store is storing user information then the file name should be user.js.

A store is defined using the defineStore function in the pinia plugin.  The defineStore function has two parameters.  The first parameter is a unique String id that is used by Vue to connect the store to Vue’s dev tools.  The second parameter, since we’re using the Composition API, is a setup function. Like with components, the setup function can contain refs, computed properties, and functions. It can also create watchers and utilize globally provided properties like the Router and Route.

defineStore returns a function that is exported.  A component that imports it can call this function to gain access to the refs and methods in the object returned by the setup function.

In the example below we create a store that holds user information in a file named user.js.  The store is named useUserStore.  This name is chosen by convention, starting with use and ending with Store.

[code language=”JavaScript”]
import { defineStore } from 'pinia'
import {ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
const firstName = ref('')
const lastName = ref('')
const userName = ref('')

const wholeName = computed(() => firstName.value + ' ' + lastName.value)

function setUser(fName, lName, uName) {
firstName.value = fName ?? ''
lastName.value = lName ?? ''
userName.value = uName ?? ''
}

return { firstName, lastName, userName, wholeName, setUser }
})
[/code]

Using the Store

We can use a store in a component by importing the store and calling the useXYZStore() function to gain access to the refs and methods in the object returned by the setup function.  The object returned by useXYZStore() is a proxy of the object returned by the setup function.  We can none-the-less access the refs and methods using the proxy.

In the code below we show how in the <script> tag of a component can call the setUser() method defined in the store.  We first import the useUserStore() function then call useUserStore() to obtain a proxy, we call userStore, and then call the setUser() method using the proxy.

[code language=”JavaScript”]
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore()
userStore.setUser('Me', 'Myself', "Irene") //firstName, lastname, userName

<script>
[/code]

We can also use the properties of the store in the template section of a component.  Below we bind an input element to a ref in the store.

[code language=”JavaScript”]
<template>

<label>First name</label>
<input type="text" v-model="userStore.firstName">

</template>
[/code]

If we want to destructure the reactive properties in the store to write a little less verbose code we must call storeToRefs() as shown below.

[code language=”JavaScript”]
<script>

import { useUserStore } from '@/stores/user';
const userStore = useUserStore()
const { firstName, lastName, userName, wholeName } = storeToRefs(userStore)
</script>

<template>

<label>Last name</label>
<input type="text" v-model="lastName">

<h1>{{ wholeName }} </h1>
….
</template>
[/code]

Resetting the State

A store defined using the Options API automatically contains a $reset() method that can be called to reset the store to its original state.  If using the Composition API, we have to manually define a $reset() function in store.  Below is a $reset() function for the user store.

[code lang=”JavaScript”]
function $reset() {
firstName.value = ''
lastName.value = ''
userName.value = ''
}
[/code]

We can get the entire active pinia state (containing all of its stores) using the getActivePinia() method.  We can then reset all of the stores in the app using the active pinia state.  Below are the contents of a composable in a file named reset.js.

[code lang=”JavaScript”]
import { getActivePinia } from 'pinia'

export function resetStores() {
const pinia = getActivePinia()
if (pinia) {
Object.keys(pinia.state.value).forEach(storeId => {
const store = pinia._s.get(storeId) // Access store instances
if (store.$reset) {
store.$reset()
}
})
}
}
[/code]

Patching a Store

Pinia automatically includes a $patch method in every store.  $patch() is used to modify the state of the store.  You can pass either an object containing the state properties that are to be modified or you can pass a function.  Below is a fragment of code that changes the firstName ref in the user store.

[code lang=”JavaScript”]
userStore.$patch((state) => {
state.firstName = 'Cecil'
})
[/code]

Observing State Changes

We can observe the changes in a store using the $subscribe() method and observe when actions (store methods) are invoked using the $onAction() method.

[code lang=”JavaScript”]
userStore.$subscribe((mutation, state) => {
// mutation type is 'direct' | 'patch object' | 'patch function'
log.value.push(`${mutation.storeId} modified via ${mutation.type}`)
})

userStore.$onAction(({ name, store, args, after, onError }) => {

after((result) => {
log.value.push(`action success: ${name}`)
})

onError((error) => {
log.value.push(`action failure: ${name}`)
})
})
[/code]