Managing State in Vue with Pinia

Introduction

In this article, we will explore Pinia, the new recommended state management library for Vue Projects. While Vuex has been traditionally the go-to library for state management in Vue, the recent trends and the fact that Vue now officially recommends Pinia as their primary choice makes it a worthy successor to the Vuex.

What is Pinia?

Pinia is a lightweight state management library, allowing sharing of state between components/pages. Pinia relies on the new reactivity system to build a system that has solid type inference when used with typescript. Compared to Vuex, Pinia provides a simpler and more intuitive API with less ceremony.

Furthermore, Pinia is built to support multiple stores. This is in contrast with Vuex, which only had a single store and relied on sub-modules to support multiple stores. Pinia al.

Getting Started with Pinia

Let us get started with Pinia. Assuming you have already created your Vue Project, let us go ahead and install Pinia.

npm install pinia

Once the package has been installed, the next step is to ensure your application uses Pinia.

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

const pinia = createPinia();
createApp(App).use(router).use(pinia).mount('#app')

As observed in the above code, we have used the createPinia() method to create an instance of Pinia and used it in our application.

Defining Store

As the next step, we need to define our Store. For the sake of this example, we will assume we want to create a store that would provide information about current users.

Each Store is defined by three key concepts - state, getters, and methods. Each stored is defined using the defineStore() method, which accepts two parameters.

The first parameter is a name for the Store. This serves as the unique identifier for the Store. The second parameter is either a setup() function or an Options object based on whether you would follow Composition API or Options API.

export const useUserStore = defineStore('UserStore', () => {
  // state
  // getters
  // methods

  return {
    /* what you want to expose */ }
});

As per the official documentation, it is a recommended convention to name the function return with a suffix used. In our case, we have defined the Store with a unique name UserStore, and they have named the function as useUserStore to align with the recommended convention.

Since we are building a User Store, let us first define a type for holding the user information in the Store.

export default interface User{
    userName : string,
    age : number,
}

For simplicity of the example, we are intending to store just the userName and the age of the User in our Store. Let us now define the first of the 3 core concepts of the Store - the state, along with initial values for the Store.

//state
const user = ref<User>({
    userName : 'John Doe',
    age : 35,
})

As you can observe, we have initialized the User with the name John Doe and age 35, respectively.

To demonstrate the usage of getters, we will add an IsSeniorCitizen getter, which would state if the current User in the Store is a senior citizen or not.

//getters
const IsSeniorCitizen = computed(()=> user.value.age >= 60);

Finally, we get to the methods. For the sake of example, we will have a Reset method that resets the Store to its initial state.

//methods
const Reset = ()=> {
    user.value = {
        userName : 'John Doe',
        age : 35,
    };
}

Here is the complete code for User Store.

import User from "@/types/user"
import { defineStore } from "pinia"
import { computed, ref } from "vue"

export const useUserStore = defineStore('UserStore',()=>{
//state
const user = ref<User>({
    userName : 'John Doe',
    age : 35,
})
//getters
const IsSeniorCitizen = computed(()=> user.value.age >= 60);
//methods
const Reset = ()=> {
    user.value = {
        userName : 'John Doe',
        age : 35,
    };
}

return {
    user, IsSeniorCitizen, Reset
};
})

Using the Store

Having defined the Store in the earlier steps, next, we have to use the Store in our components. We can fetch the instance of the Store using the method we created in the store useUserStore. Note that in the earlier step, we have only defined the Store. The Store would not be created unless the useUserStore is called for the first time.

<script setup lang="ts">
import { useUserStore } from "@/stores/userStore";

const userStore = useUserStore();
</script>

Once the Store is instantiated, you can use the state, getters, and methods exposed to access the information required from the Store.

While we could use access the state using the userStore defined in the above code, it is sometimes desirable to destructure the Store to extract the properties. This is done using the storeToRefs, which helps to retain the reactivity of the properties. For example

import { storeToRefs } from "pinia";
const { user } = storeToRefs(userStore);

The complete code for using the Store is as follows.

<script setup lang="ts">
import { ref } from "vue";
import { useUserStore } from "@/stores/userStore";
import { storeToRefs } from "pinia";

const userStore = useUserStore();
const { user } = storeToRefs(userStore);
const { IsSeniorCitizen } = userStore;

const resetUserStore = () => {
    userStore.Reset();
}
</script>

Note that we have defined and used Reset(), the method in the Store. This is because we have used Setup Store, or in other words, we defined our Store using Setup as a second parameter in defineStore. If you are, instead, using Options Store, you can reset the Store using the inbuilt $reset().

You have now defined and consumed your User store. Similarly, you can define multiple stores. Vue recommends defining each Store in a separate file.

Closing Notes

As you can observe, Pinia provides a simpler Api compared to Vuex. Pinia also supports multiple stores, each of which can be used independently (and in one another as well - we can discuss it in a different article). With less ceremony, it is definitely easier to use, and it almost feels like you are not using a store at all.

The complete example code discussed in this article can be accessed from my Github.