DEV Community

Cover image for Getting started with Vue 3 + Pinia Store + TypeScript by building a Grocery List App
Carlo Miguel Dy
Carlo Miguel Dy

Posted on

Getting started with Vue 3 + Pinia Store + TypeScript by building a Grocery List App

Introduction

Let's build a grocery list application using Vue 3 with Typescript and the Vue store Pinia! I just found out that Edward have tweeted about publishing the documentation for Pinia so I thought I'd share how we can create a simple application using this store.

I will only be covering the very basic implementation of the Pinia store.

Pre-Requisites

This article assumes the basic knowledge and understanding or familiarity with:

  • Vue 3 (Composition API)
  • TypeScript
  • Prior understanding of what State Management is

I will be using TypeScript for this application so I hope you understand at least the basic type annotations. Otherwise let's get right at it and start building this app!

Installation

If you don't have the Vue CLI installed yet then make sure to install it, or if your Vue CLI isn't updated yet then make sure it is on the latest version.

$ npm i -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

If you are on a Linux distro then add sudo at the beginning of the command since we are installing Vue CLI globally.

And once that is done let's ask Vue CLI to scaffold a Vue 3 project for us. Make sure you have selected Vue 3.

$ vue create vue-3-pinia-grocery-app
Enter fullscreen mode Exit fullscreen mode

And once that is done navigate into the app and open the project in your IDE.

$ cd vue-3-pinia-grocery-app && code .
Enter fullscreen mode Exit fullscreen mode

Then let us add our sugar, TypeScript.

$ vue add typescript
Enter fullscreen mode Exit fullscreen mode

For now these are my selected options, you can choose on your own if you prefer.

image

Next is to install Pinia as the dependency for this project.

$ npm install pinia@next
Enter fullscreen mode Exit fullscreen mode

And lastly install faker since I am kind of lazy to create forms for this tutorial and creating a form and validating it is sort of an out of scope. So to make things quick, let's just generate some random data from this faker package.

$ npm install faker
$ npm install --save @types/faker

$ npm install uuid
$ npm install --save-dev @types/uuid
Enter fullscreen mode Exit fullscreen mode

Since I plan to use some fake data for quick data generation. Update your model code as I will have a method called generateFakeData() to generate an Item.

import { v4 as uuidv4 } from "uuid";
import * as faker from "faker";

export interface Item {
  id: string;
  name: string;
  description?: string;
  quantity: number;
  createdAt: Date;
  deletedAt?: Date;
}

export function generateFakeData(): Item {
  return {
    id: uuidv4(),
    quantity: Math.random(),
    name: faker.lorem.word(),
    description: faker.lorem.words(),
    createdAt: new Date(),
  };
}
Enter fullscreen mode Exit fullscreen mode

And once that is done let us run our Vue application.

$ npm run serve
Enter fullscreen mode Exit fullscreen mode

Data Model

Since we are building a Grocery List application then we should model our data. The core model to have is an Item.

So to define the model,

export interface Item {
  id: string;
  name: string;
  description?: string;
  quantity: number;
  createdAt: Date;
  deletedAt?: Date;
}
Enter fullscreen mode Exit fullscreen mode

So under the src directory create a models directory and it's where this Item model will reside. So create a file name it as item.model.ts.

Then we'll have the following,

image

Pinia Setup

Open the main.ts file under the src directory and make sure to chain the following method use() and pass in createPinia() as the first parameter.

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

createApp(App)
  .use(createPinia())
  .mount("#app");
Enter fullscreen mode Exit fullscreen mode

Next is to create a directory and name it as store and inside it create a file called index.ts

Then to define this main store,

import { generateFakeData, Item } from "@/models/item.model";
import { defineStore } from "pinia";

export type RootState = {
  items: Item[];
};

export const useMainStore = defineStore({
  id: "mainStore",
  state: () =>
    ({
      items: [],
    } as RootState),

  actions: {
    createNewItem(item: Item) {
      if (!item) return;

      this.items.push(item);
    },

    updateItem(id: string, payload: Item) {
      if (!id || !payload) return;

      const index = this.findIndexById(id);

      if (index !== -1) {
        this.items[index] = generateFakeData();
      }
    },

    deleteItem(id: string) {
      const index = this.findIndexById(id);

      if (index === -1) return;

      this.items.splice(index, 1);
    },

    findIndexById(id: string) {
      return this.items.findIndex((item) => item.id === id);
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

We have defined the most basic functionality, creating, updating an deleting an item from our grocery list. And that is more done enough for getting to know how to setup Pinia as your Vue store.

Demo

This is the best looking UI... Yeah.

image

Demo

As you can see from this setup we are able to use the Pinia store, that we are able to add an Item, update it and delete it.

Summary

We learned how to setup Pinia with Vue 3 and TypeScript. What I like Pinia is that it is built with TypeScript already that means the store provides us all the auto-completion that we want and the reason we love about TypeScript. Pinia is also very intuitive which we notice it was very similar to how Vuex is implemented.

But there are more of its features that you can read about from the official documentation

I hope you find this useful, cheers!

Full source code can be found from the repository

Top comments (5)

Collapse
 
cefn profile image
Cefn Hoile

Hi, Carlo. Thought it was worth pointing out a latent bug in the example. Probably as RootState should never be used. You can see and experiment with the error at tsplay.dev/WkM3lN

Essentially the clause as RootState is a type assertion, when what is probably intended is for that data structure to be validated as a RootState by the compiler. Normally the keywords as and any are a sign types are being bypassed (with the exception of as const).

The type assertion is hiding an error in the stateFactory2 example below, but the compiler can't pick that up, since you have asserted the type and hence suspended the validation of the compiler.

The example stateFactory3 below is preferred, and DOES reveal the bug as a compiler error.

interface RootState {
    message:string|null
}
const stateFactory1 = () => ({message:null} as RootState);
const stateFactory2 = () => ({message:"something", hmm:4} as RootState);
const stateFactory3 = (): RootState => ({message:"something", hmm:4});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
carlomigueldy profile image
Carlo Miguel Dy

Hey Cefn, thank you so much for pointing this out! I haven't actually gotten into details regarding this so I highly appreciate your comment as it helps out for the other readers as well

Collapse
 
bennyxguo profile image
Benny Guo

I used Pinia for my project as well, just loved it.

Collapse
 
carlomigueldy profile image
Carlo Miguel Dy

It's so great and straightforward!

Collapse
 
geewhizbang profile image
Geoffrey Swenson

Is it required for the component to return RootState or can it be any type you want?