Categories
JavaScript

Pinia: A lightweight Vuex alternative

Note: This article assumes that you have a basic understanding of state management.

A few days ago, I was creating a new Vue 3 project and as usual, I chose Vuex from the interactive project initializer. Also, I’ve been using TypeScript with Vue since Vue 2, so this one is no exception. As I started working on the auth feature, I realized that it’s been more than half an hour and I’m still setting up the simple auth Vuex module.

To give you an idea of how a typical store setup looks like in Vuex 4 with Vue 3 and TypeScript, here is a demo project.

Now, I don’t oppose complexity in general, but that complexity should be worth my time and effort. All I wanted is centralized state management with DevTools support and writing all that boilerplate code just to achieve that is not my cup of tea.

So I had two options:
a) Give up TypeScript and go back to JavaScript
b) Give up Vuex and write my own state management with Composition API.

Option a is a trade-off I was not willing to make and option b wouldn’t have the DevTools support. So in despair, I googled and found this awesome, lightweight, and type-safe library called Pinia.

Installation

The installation is as simple as adding an npm dependency to your existing project.

yarn add pinia@next
# or with npm
npm install pinia@next

This will add Pinia to your Vue 3 project. Pinia can also be installed in Vue 2 project. For that, you’ll have to install pinia@latest and @vue/composition-api dependencies to your Vue 2 project. We’ll use Vue 3 in our example.

Alright, let’s take a look at how we could implement a simple todos store using Pinia.

Store SetUp

In our example, each todo is going to have 3 properties:
text (The task)
id (The unique identifier)
isFinished (Whether the task is finished or not)

Let’s create a todo.ts file inside the types directory and define the interface for a Todo so that we can get type-safety and autocompletion support:

// @/types/todo.ts

export interface Todo {
    text: string,
    id: number,
    isFinished: boolean
}

Now let’s create the actual todos store inside @/store/todos.ts file:

// @/store/todos.ts

import { defineStore } from 'pinia';
import { Todo } from "@/types/todo";

export const todos = defineStore({
    id: 'todos',
    state: () => ({
        todos: [] as Todo[],
        nextId: 0
    }),
    getters: {
        finishedTodos() {
            // autocompletion! ✨
            return this.todos.filter((todo) => todo.isFinished);
        },
        unfinishedTodos() {
            return this.todos.filter((todo) => !todo.isFinished);
        }
    },
    actions: {
        // any amount of arguments, return a promise or not
        addTodo(text: string) {
            // you can directly mutate the state
            this.todos.push({ text, id: this.nextId++, isFinished: false });
        },
        finishTodo(id: number) {
            this.todos.find(todo => todo.id == id)!.isFinished = true;
        }
    }
});

First, we import the defineStore() method from pinia and then define a store passing an options object as an argument to that function. Each store should have an id, in our case, it’s just todos.

And then state, getters and actions. If you’re familiar with Vuex, you’ll immediately notice that there is no mutations section. That’s right. Pinia doesn’t require separate mutations. You can mutate the states directly inside actions.

We only have two states:
todos (The list of todos. Typed with the Todo interface we created earlier)
nextId (The incremental id)

Similarly, we have two getters:
finishedTodos (The filtered list of finished todos)
unfinishedTodos (The filtered list of unfinished todos)

Finally, we have two actions:
addTodo (Adds a todo to the list)
finishTodo (Marks a todo as finished)

Usage

You can use Pinia with both the Options API and the Composition API.

To use Pinia with Options API, you will need to import the map functions (mapState, mapActions) from the pinia module, just like you will do in Vuex. In this example, however, we’ll see how we can use it with Composition API:

In any of your Vue components, import the store you’d like to use:

import { todos } from '@/store/todos';

It will be imported as a function. So you’ll need to invoke it and save the store in a variable like this:

const todo = todos();

And now you have access to all the state, getters, and actions of that store. Let’s add two todos and mark one of them as finished:

// App.vue

<script lang="ts">
import { defineComponent } from "vue";
import { todos } from '@/store/todos';

export default defineComponent({
  setup() {
    const todo = todos();
    todo.addTodo('Todo 0');
    todo.addTodo('Todo 1');
    todo.finishTodo(1);
    console.log(todo.finishedTodos);
    // {id: 1, isFinished: true, text: "Todo 1"}
  }
});
</script>

Finally, this is how the store is represented in the Vue DevTools under the Pinia menu:

For more information about this library, visit the official site of Pinia.

By Tanmay Das

A web artisan who builds web services and applications applying modern software development methodologies. Also a TDD fan and a *nix lover.

One reply on “Pinia: A lightweight Vuex alternative”

I was following your code on SandBox because I was looking for a solution on VUEX when you have multiple store modules. The error occurs when you have child stores and you are trying to commit an action from parent store with the wrong parameters – This is related to Namespacing modules. I finally gave up on VUEX and also didn’t wanted to gave up TS so ended up using Pinia… good decision so far.
Thanks for the effort and for sharing…
cheers!!

Leave a Reply

Your email address will not be published. Required fields are marked *