<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development
Loading

Bitovi |

How to Get Started with Pinia in Vue

Pinia, the lightweight state management library, and Vue make a great team! Get started using Pinia in Vue with this quick tutorial.

Eduard Krivanek

Eduard Krivanek

Twitter Reddit

You are starting your VueJs project and you get stuck on a question: how am I going to share data across multiple components? While there are a variety of lightweight state management libraries for Vue, Pinia is the one officially recommended by the Vue team, that solves sharing data across components intuitively.

On one hand, the most used library in Vue is Vuex; however, Pinia has almost the exact same or enhanced API as Vuex 5, described in Vuex 5 RFC. Pinia can be considered as a successor of Vuex with a different name and is also compatible with Vue 2.x.

Why Pinia?

Pinia has solid type inference support when used with TypeScript and provides a simpler API that led it to become the new recommended state management library by Vue. Looking at the NPM download and the almost 9K stars on the Pinia Github repo, Pinia is gaining popularity.

Comparison, such as Pinia against Vuex, shows that Vuex, the previously recommended library, still has a higher download rate than Pinia. However, even the Vuex official documentation advises developers to opt-in for Pinia, since it is unlikely that Vuex will receive any additional functionalities.

How to Install Pinia

Let’s take a look at how Pinia works. Installing and opting for Pinia is relatively straightforward. Follow the example below:

npm install pinia
// main.js or main.ts

import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";

createApp(App)
.use(createPinia())
.mount('#app')

How Storage Works

Pinia’s approach to handling global storage is based on modularity. We create multiple smaller stores, as it makes sense for our application, give them a unique identification key and in the end, Pinia combines each store into a single one. A store definition consists of four properties:

  • id - a unique key that identifies part of the application state
  • state - function that returns an initial state
  • getters - a place to define compute values for the state of the Store
  • actions - a place to define methods for mutation of the Store
import { defineStore } from "pinia";

export const useTodoStore = defineStore({
  id: "uniqueID",
  state: () => ({
    // ...
  }),
  getters: {
    // ...
  },
  actions: {
    // ...
  },
});

Pinia is also supported in Vue.js devtools, where, as mentioned before, the global state object is composed of multiple modular defineStore({...}) objects with a unique key.

pinia root

Example Application

Let’s create a todo application, consisting of the following steps:

  1. Creating a Todo item form in TodoForm.vue
  2. Persisting a newly created Todo in the global store
  3. Displaying a list of Todo items from the global store in TodoList.vue

The whole source code can be found in the following GitHub repository and the end result is shown in the provided gif.

pinia vue todo app

By using TypeScript, first, you create an interface representing your Todo items.

// types/todo.ts

export interface ITodo {
  id: number;
  title: string;
  description?: string;
}

Setting Up State Storage

Representation of our Todo storage is shown in the following snippet.

In the action section, you implement two mutation methods that will update the store state. The first one is addTodo, which adds a new Todo item to the list of todos, and the second one is removeTodo, which removes one by its Id. Mutations can access the store state by using the keyword this, so in your case, you access todos by this.todos.

It is also worth highlighting the removeTodo method to see that asynchronous mutations are performed in the same way as synchronous ones like the addTodo method in your case.

// store/todo.ts

import type { ITodo } from "@/types/todo";
import axios from "axios";
import { defineStore } from "pinia";

export const useTodoStore = defineStore({
  id: "todoState",
  state: () => ({
    todos: [] as ITodo[],
  }),
  getters: {
    totalTodos: (state) => state.todos.length,
  },
  actions: {
    addTodo(title: string, description?: string) {
      const todo: ITodo = {
        id: Math.floor(Math.random() * 10000), // random ID
        title,
        description,
      };
      this.todos = [todo, ...this.todos];
    },

    async removeTodo(id: number) {
      // example of an async request
      const response = await axios.get(
        "https://www.random.org/integers/?num=1&min=1&max=100&col=5&base=10&format=plain",
      );
      console.log("received data ", response.data);

      // remove todos
      this.todos = this.todos.filter((todo) => todo.id !== id);
    },
  },
});

Display Todos

Both components TodoForm.vue and TodoList.vue are basic Vue components. The main concept of both of them is the setup() method, which serves as an entry point to the component, where we store the reference to the todo store by const storeTodo = useTodoStore() so that it becomes available for the whole component.

// components/TodoForm.vue

<template>
  <form @submit="onSubmit">
    <h2>Todo Form</h2>

    <!-- title -->
    <div class="field">
      <label class="label">Title</label>
      <input type="text" class="input" name="title" v-model="title" />
    </div>

    <!-- description -->
    <div class="field">
      <label class="label">Description</label>
      <textarea class="input" name="description" v-model="description"></textarea>
    </div>

    <!-- submit -->
    <div class="field">
      <button type="submit">Create Todo</button>
    </div>
  </form>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { useTodoStore } from "../stores/todo";

export default defineComponent({
  name: "TodoForm",
  data() {
    return {
      title: "",
      description: "",
    };
  },
  setup() {
    const storeTodo = useTodoStore();
    return { storeTodo };
  },
  methods: {
    onSubmit(e: Event) {
      e.preventDefault();

      if (!this.title) {
        return;
      }

      // save data into store
      this.storeTodo.addTodo(this.title, this.description);
    },
  },
});
</script>

<style scoped>
/* ... */
</style>
// components/TodoList.vue

<template>
  <div>
    <div v-for="todo of storeTodo.todos" :key="todo.id" class="wrapper">
      <div class="header">
        <div class="title">[{{ todo.id }}] {{ todo.title }}</div>
        <div>
          <button type="button" @click="storeTodo.removeTodo(todo.id)">
            remove
          </button>
        </div>
      </div>
      <div v-if="todo?.description">{{ todo?.description }}</div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { useTodoStore } from "../stores/todo";

export default defineComponent({
  name: "TodoList",
  setup() {
    const storeTodo = useTodoStore();

    return { storeTodo };
  },
});
</script>

<style>
/* ... */
</style>

Summary

Even as a lightweight state management library, Pinia has become the recommended library both by Vue and Vuex developers and it is maintained by the core Vue team.

We took a look at how to write and read data from storage by a basic Todo application. Pinia can be used for both Vue 2 and Vue 3 users and from small to large-scale applications. Need help with state management or setting up Pinia for Vue? Book a session for Vue Consulting or check out our training program Bitovi Academy.

Do you have thoughts?

We’d love to hear them! Join our Community Discord to continue the conversation.