Pinia and Vue 3 first steps

Take the first steps with Pinia and Vue 3 using Composition API. It is a lightweight, extensible, and modular library that takes away the complexity of Vuex.


If you've worked with Vue 2, you've probably used Vuex: Vue's official solution for state management.

Now, with Vue 3 and its Composition API, we don't have a new version of Vuex but its replacement: Pinia. In this DevTip, you'll learn to take the first steps with Pinia.

But what exactly is Pinia?

What is Pinia?

Pinia removes much of the complexity of Vuex in several areas:

  • There are no modules as such, but different stores (like the leaves of a pineapple 🍍) that can easily communicate.
  • There are no mutations because they are not necessary to track state changes (with the exposed Vue 3 reactivity API, we can mutate state from anywhere).
  • It's much easier to import our stores into any component or composable.
  • Better (default) TypeScript support.
  • Native support for Vue DevTools 🔥.
  • Extremely lightweight: around 1.5kb.

I suggest you take a look at the official Pinia documentation.

Using Vue 3 and Pinia

Installing Pinia

First, of course, install Pinia and add it to our setup. So yarn add pinia and then:

JS
// main.js
import {createApp} from 'vue'
import {createPinia} from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')

Creating a store with Pinia

Now we can create our store, for example, in stores/counter.js. Here we will use the Pinia defineStore method to create a new store with at least the options property for the state, an increment action to mutate it, and a totalClicks getter that will return a computed value.

JS
// stores/counter.js
import {defineStore} from 'pinia';
export const useCounterStore = defineStore('counter', {
    state: () => ({
        options: {
            a: 0,
            b: 0,
            c: 0,
        },
    }),
    actions: {
        increment(option) {
            this.options[option]++;
        },
    },
    getters: {
        totalClicks() {
            return Object.values(this.options).reduce((total, current) => {
                return total + current;
            }, 0);
        },
    },
});

Using a store created with Pinia

To use a store created with Pinia, we just have to import it into any component or Vue 3 composable, so let's import useCounterStore and then execute the function that will return the store.

We save all of this in the store constant.

<!-- Component.vue -->
<script setup>
    import {useCounterStore} from '../stores/counter';
    const store = useCounterStore();
</script>

Now we can use its properties: state, actions, and getters. By using the script setup syntax, we have direct access to all of them in the template.

Notice how we interpolate the reactive getter totalClicks, display the value of each options property with store.options[option], and call the store.increment[option] action as if it were another method (which it is).

<!-- Component.vue -->
<template>
    <h2>Total Clics: {{ store.totalClicks }}</h2>
    <div class="card">
        <button type="button" @click="store.increment('a')">
            Option A clicked {{ store.options['a'] }} times
        </button>
        <button type="button" @click="store.delayedIncrement('b')">
            Option B clicked {{ store.options['b'] }} times
        </button>
        <button type="button" @click="store.increment('c')">
            Option C clicked {{ store.options['c'] }} times
        </button>
    </div>
</template>

Asynchronous Actions with Pinia

Just like with Vuex, Pinia actions can also be asynchronous. Imagine a case where you make an HTTP request and have to wait for the response to update the state or call other actions.

Let's create delayedIncrement to wait for a number of milliseconds before updating options.

As simple as this:

JS
// stores/counter.js
const wait = (time = 1000) =>
    new Promise((resolve) => setTimeout(resolve, time));
export const useCounterStore = defineStore('counter', {
    ...
        actions
:
{
...
    async
    delayedIncrement(option)
    {
        await wait();
        this.options[option]++;
    }
,
}
,
...
})
;

Destructuring Pinia with storeToRefs

Just like with the Vue 3 reactive API, we cannot destructure the resulting values when instantiating our store. This won't work:

JS
const store = useCounterStore();
const {options, increment, totalClicks} = useCounterStore(); // ❌

To avoid losing reactivity in your store, use the Pinia helper storeToRefs:

JS
import {storeToRefs} from 'pinia'
const store = useCounterStore();
const {options, increment, totalClicks} = storeToRefs(store); // ✅

Conclusion

  • You can see how easy it is to take the first steps with Pinia and Vue 3.
  • Happy coding!🔥

Demo

Juan Andrés Núñez
Juan Andrés Núñez
Frontend Engineer. Vue.js Specialist. Professional Teacher. Stoic.