03Dec
Building Web Apps with Vue 3 composition API + Typescript + Vuex(4.0)
Building Web Apps with Vue 3 composition API + Typescript + Vuex(4.0)

Introduction

In this tutorial, we are going to create a task management application to demonstrate how to build applications with the new Vue 3 composition API, typescript, and Vuex(4.0). Furthermore, we will explore Vuex(4.0) practically.

Pre-requisite

Node.js 10.x and above
Knowledge of JavaScript Es6
Basic knowledge of vue.js
Basic knowledge of Typescript

Introducing Vue composition API

The composition API is a new API for creating components in vue 3.
It presents a clean and flexible way to compose logic inside and between components. Also, it solves the problems associated with using mixins and higher-order component to share re-usable logic between components. It has a small bundle size and naturally supports typescript.

Introducing Vuex(4.0)

Vuex is a state management library created by the Vue team and built solely for use with Vue. It provides an intuitive development experience when integrated into an existing Vue app. Vuex becomes crucial when your Vue app gets more complex as it grows.

The latest version of Vuex, version v4.0.0 supports the new Composition API introduced in Vue 3 as well as a more robust inference for TypeScript.
In this article, we will explore the latest version of Vuex, version v4.0.0 practically as we build along.

Setting up vue 3 + vuex + TypeScript App

Let’s start by creating a Vue 3 app with typescript support using the Vue-CLI tool.

Take the following steps to create a Vue 3 + Vuex app with Typescript support, using Vue CLI tool

  1. Install the Vue-CLI tool globally by executing the command below:
npm install --global @vue/cli

2. If you have the CLI already you can check if you have the appropriate version (v4.x):

vue --version

3.  Make sure you upgrade it to the latest version with the following command:

npm update -g @vue/cli
# OR
yarn global upgrade --latest @vue/cli

Create a new Vue 3 app with typescript and Vuex support with the command below:

vue create vuex-ts-project

Choose the manually select feature option
Hit the space key to select the following options:

  • choose Vue version
  • Babel
  • TypeScript
  • Linter / Formatter

Next, Choose Vue 3.x(Preview) as the version for the project.

  • Enter yes to use the class-style component syntax
  • Enter yes to use Babel alongside TypeScript
  • Select any linter of your choice
Project setup options
Project setup options

Your final setup options should be like the above.

Once the Vue 3 app is generated successfully, we will have a Vue 3 project setup with vuex (V4.x) and full typescript support.

To start the development server, run the command below in your terminal and head over to http://localhost:8080 to preview your project in the browser.

npm run serve
Result of Initial project scaffold
Result of Initial project scaffold

Next, open the vuex-ts-project folder in VS code editor or any other code editor with typescript support.

For developing Vue applications with TypeScript, the Vue core team strongly recommends using Visual Studio Code or WebStorm which provides great out-of-the-box support for TypeScript and Vue. Hence in this tutorial, we will use Visual Studio Code.

Overview of what we will build in this tutorial

In this tutorial we will build a task management application with the following features:

  • Create task
  • Edit task
  • View task
  • Delete task

Dependency Installation

We will use the Bulma CSS framework for styling our application. Run the command below to install Bulma CSS:

npm install bulma

Head over to App.vue and import Bulma CSS as follows:

//App.vue
<style lang="scss">
@import '~bulma/css/bulma.css';
</style>

Setting up our Vuex store

In Vuex, the state of the application is kept in the store. The application state can only be updated by dispatching the actions within a component which triggers the mutations in the store. The Vuex store is made up of the state, mutations, actions, and getters.

We’ll start building our application by first building the Vuex store with TypeScript for type safety. We already know that a fully-defined Vuex store is composed of 4 distinct pieces – state, mutations, actions, and getters. We’ll set up the store in steps:

  1. Create our state objects
  2. Set up the mutations that will occur in our application
  3.  Create the actions that will commit to these subsequent mutations
  4. Create getters for components to directly compute state data

State

A state is a store object that holds the application-level data that needs to be shared between components.

Head over to the store folder and create a state.ts file inside it. Add the following code below to state.ts:

// /store/state.ts
export type TaskItem = {
  id: number;
  title: string;
  description: string;
  createdBy: string;
  assignedTo: string;
  completed: boolean;
  editing: boolean;
};
export type State = {
  loading: boolean;
  tasks: TaskItem[];
  showCreateModal: boolean;
  showEditModal: boolean;
  showTaskModal: boolean;
  editModalTaskId: number | undefined;
  showTaskId: number | undefined;
};
export const state: State = {
  loading: false,
  tasks: [],
  showCreateModal: false,
  showEditModal: false,
  showTaskModal: false,
  editModalTaskId: undefined,
  showTaskId: undefined,
};

Here, we add some type of safety to the TaskItem and State by defining their types. We also export types because they will be used in the definitions of getters, mutations and actions. Finally, we cast the State type to the state.

Mutations

Mutations are methods that modify the store state. It accepts state as the first argument and payload as the second, and eventually modifies the store state with the payload.
To create mutations, we will use constants for mutation types as recommended by the Vuex docs.

Still in the store folder, create a mutations.ts file with the following code below:

// /store/mutations.ts
import { MutationTree } from 'vuex'
import { State, TaskItem } from './state'

export enum MutationType {
  CreateTask = 'CREATE_TASK',
  SetTasks = 'SET_TASKS',
  CompleteTask = 'COMPLETE_TASK',
  RemoveTask = 'REMOVE_TASK',
  EditTask = 'EDIT_TASK',
  UpdateTask = `UPDATE_TASK`,
  SetLoading = 'SET_LOADING',
  SetCreateModal = 'SET_CREATE_MODAL',
  SetEditModal = 'SET_EDIT_MODAL',
  SetTaskModal = 'SET_TASK_MODAL'
}

MutationTree is a generic type, that is shipped with the Vuex package. it helps to declare a type of mutation tree. We will use it as we proceed.
Here, all of our possible names of mutations are stored in the MutationTypes enum.

Next, we will declare a contract (types) for each MutationType
Add the code below to the mutation.ts file:

//mutations.ts
export type Mutations = {
  [MutationType.CreateTask](state: State, task: TaskItem): void
  [MutationType.SetTasks](state: State, tasks: TaskItem[]): void
  [MutationType.CompleteTask](
    state: State,
    task: Partial<TaskItem> & { id: number }
  ): void
  [MutationType.RemoveTask](
    state: State,
    task: Partial<TaskItem> & { id: number }
  ): void
  [MutationType.EditTask](
    state: State,
    task: Partial<TaskItem> & { id: number }
  ): void
  [MutationType.UpdateTask](
    state: State,
    task: Partial<TaskItem> & { id: number }
  ): void
  [MutationType.SetLoading](state: State, value: boolean): void
  [MutationType.SetCreateModal](state: State, value: boolean): void
  [MutationType.SetEditModal](state: State, value: {showModal: boolean, taskId: number|undefined}): void
  [MutationType.SetTaskModal](state: State, value: {showModal: boolean, taskId: number|undefined}): void
}

Next, we will implement the contract (types) declared for each MutationType :
Add the code below to the mutation.ts file:

// /store/mutation.js
export const mutations: MutationTree<State> & Mutations = {
  [MutationType.CreateTask](state, task) {
    state.tasks.unshift(task)
  },
  [MutationType.SetTasks](state, tasks) {
    state.tasks = tasks
  },
  [MutationType.CompleteTask](state, newTask) {
    const task = state.tasks.findIndex(element => element.id === newTask.id)
    if (task === -1) return
    state.tasks[task] = { ...state.tasks[task], ...newTask }
  },
  [MutationType.RemoveTask](state, Task) {
    const task = state.tasks.findIndex(element => element.id === Task.id)
    if (task === -1) return
    //If Task exist in the state, remove it
    state.tasks.splice(task, 1) 
  },
  [MutationType.EditTask](state, Task) {
    const task = state.tasks.findIndex(element => element.id === Task.id)
    if (task === -1) return
    //If Task exist in the state, toggle the editing property
    state.tasks[task] = { ...state.tasks[task], editing: !state.tasks[task].editing } 
    console.log("taskino", state.tasks[task])
  },
  [MutationType.UpdateTask](state, Task) {
    state.tasks = state.tasks.map(task => {
      if(task.id === Task.id) {
        return {...task, ...Task}
      }
      return task;
    })
  },
  [MutationType.SetLoading](state, value) {
    state.loading = value
    console.log("I am loading...")
  },
  [MutationType.SetCreateModal](state, value) {
    state.showCreateModal = value
  },
  [MutationType.SetEditModal](state, value) {
    state.showEditModal = value.showModal
    state.editModalTaskId = value.taskId
  },
  [MutationType.SetTaskModal](state, {showModal, taskId}) {
    state.showTaskModal = showModal
    state.showTaskId = taskId
  }
}

Here, we create the mutations variable which stores all the implemented mutations. The MutationTree<State> & Mutations ensures that the contract is implemented correctly else Typescript will trigger an error.

Let’s assume that we failed to implement the contract below:

[MutationType.CreateTask](state: State, task: TaskItem): void

By adding comments the mutation as seen below:

export const mutations: MutationTree<State> & Mutations = {
  // [MutationType.CreateTask](state, task) {
  //    state.tasks.unshift(task)
  // },
}

Here typescript IntelliSense will highlight const mutations in red and Hovering on the highlight we will get the following typescript error:

Property '[MutationType.CreateTask]' is missing in type 
'{ SET_TASKS(state: State, tasks: TaskItem[]): void; 
COMPLETE_TASK(state: State, newTask: Partial<TaskItem> & { id: number; }): void; ... 6 more ...; 
SET_TASK_MODAL(state: State, { showModal, taskId }: { ...; }): void; }'
 but required in type 'Mutations'.ts(2322)

Actions

Actions are methods that trigger the mutations. When handling asynchronous tasks, actions are used before calling the corresponding mutations. We will simulate asynchronous tasks as we create our actions.

Still in the store folder, create an actions.ts file with the following code below:

// /store/action.ts
import { ActionContext, ActionTree } from 'vuex'
import { Mutations, MutationType } from './mutations'
import { State } from './state'

export enum ActionTypes {
  GetTaskItems = 'GET_Task_ITEMS',
  SetCreateModal = 'SET_CREATE_MODAL',
  SetEditModal = 'SET_EDIT_MODAL'
}

Similarly, all of our possible names of actions is stored in the ActionTypes enum.
Next, we will declare a contract (types) for each ActionType

Add the code below to the action.ts file:

// /store/action.ts
type ActionAugments = Omit<ActionContext<State, State>, 'commit'> & {
  commit<K extends keyof Mutations>(
    key: K,
    payload: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>
}
export type Actions = {
  [ActionTypes.GetTaskItems](context: ActionAugments): void
  [ActionTypes.SetCreateModal](context: ActionAugments): void
  [ActionTypes.SetEditModal](context: ActionAugments): void
}

Here we use the ActionContext type which is shipped with the Vuex package, in the
ActionAugments type to restrict commits only to the declared mutations and also to check the payload type.

Next, we will implement the contract (types) declared for each ActionType
Add the code below to the action.ts file:

//action.ts
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
export const actions: ActionTree<State, State> & Actions = {
  async [ActionTypes.GetTaskItems]({ commit }) {
    commit(MutationType.SetLoading, true)
    await sleep(1000)
    commit(MutationType.SetLoading, false)
    commit(MutationType.SetTasks, [
      {
        id: 1,
        title: 'Create a new programming language',
        description: "The programing language should have full typescript support ",
        createdBy: "Emmanuel John",
        assignedTo: "Saviour Peter",
        completed: false,
        editing: false
      }
    ])
  },
  async [ActionTypes.SetCreateModal]({ commit }) {
    commit(MutationType.SetCreateModal, true)
  },
  async [ActionTypes.SetEditModal]({ commit }) {
    commit(MutationType.SetEditModal, {showModal: true, taskId: 1})
  }
}

Here we create a sleep variable that returns Promise.  Followed by the actions variable which stores all the implemented actions. Similarly, the ActionTree<State> & Actions ensures that the contract (type Actions) is implemented correctly else Typescript will trigger an error.

Next, we set up our asynchronous call in the GetTaskItems action and commit an array containing a single task object. we also set up SetCreateModal and SetEditModal actions.

Getters

Getters are methods that receive the state as its first parameter and return computed information from the store state.
The only getters we’ll need in our store are:

  • completedTaskCount – A function that gets the total number of completed task in our state
  • totalTaskCount – A function that gets the total number of task in our state
  • getTaskById – A function that gets a task by its id.

Still in the store folder, create a getters.ts file with the following code below:

// store/getters.ts
import { GetterTree } from 'vuex'
import { State, TaskItem } from './state'

export type Getters = {
  completedTaskCount(state: State): number
  totalTaskCount(state: State): number
  getTaskById(state: State): (id :number) => TaskItem | undefined
}

export const getters: GetterTree<State, State> & Getters = {
  completedTaskCount(state) {
    return state.tasks.filter(element => element.completed).length
  },
  totalTaskCount(state) {
    return state.tasks.length
  },
  getTaskById: (state) => (id: number) => {
    return state.tasks.find(task => task.id === id)
  }
}

Similarly, we have added some type of safety to Getters.

Store

Now that our state, mutations, actions, and getters are all set-up, let’s wire them to the global Vuex store.
The main Vuex store now needs to import these pieces ( state, mutations, actions, and getters) and include them within the createStore method.

We’ll update store/index.ts to reflect this:

//store/index.ts
import {
  createStore,
  Store as VuexStore,
  CommitOptions,
  DispatchOptions,
  createLogger
} from 'vuex'
import { State, state } from './state'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'
import { Getters, getters } from './getters'

export const store = createStore<State>({
  plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : [],
  state,
  mutations,
  actions,
  getters
})
export function useStore() {
  return store as Store
}
export type Store = Omit<
  VuexStore<State>,
  'getters' | 'commit' | 'dispatch'
> & {
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>
} & {
  dispatch<K extends keyof Actions>(
    key: K,
    payload?: Parameters<Actions[K]>[1],
    options?: DispatchOptions
  ): ReturnType<Actions[K]>
} & {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>
  }
}

Here we create a store using the createStore() method from Vuex. We trigger the createLogger plugin which logs the state (previous state and next state) and mutations to the console. Export the useStore function for use in the application components.
Now we have access to a fully typed store all through our application.

To have the store accessible in all our application’s components, we need to inject it into the entire application. Fortunately, the Vue-CLI tool had already imported the entire store and passed it within the application’s Vue instance.

// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import {store} from './store'
createApp(App).use(store).mount('#app')

Connecting the component’s logic with the Vuex store

Now that the store is properly set up with full type support we will look at how to use the store in our app components using the composition API.

App

For us to retrieve information from the store, we first need to invoke the mutation that sets the hardcoded task in the store. Since we need the mutation to occur at the moment the component is mounted, we’ll dispatch the GetTaskItems action within the component’s mounted() hook.
In order to use the store in a component, we must access it via the useStore hook, which will return our store.

We will create three computed properties:

  •  completedCount – that calls the completedTaskCount getter method to retrieve the total number of completed tasks
  • totalCount – that calls the totalTaskCount getter method to retrieve the total number of tasks
  • loading – that gets the loading property of the state

Update App.vue with the following code below:

//App.vue
<template>
  <div class="container mx-auto mt-4">
    <h1 class="is-size-3 has-text-centered p-2 has-text-weight-bold">
      Vue 3 Task Management App with Typescript and Vuex 4
    </h1>
    <div v-if="loading">
      <h3 class="has-text-centered mt-4">Loading...</h3>
    </div>
    <div v-else>
      <p class="has-text-centered mt-2">
        {{ completedCount }} of {{ totalCount }} completed.
      </p>
      <TaskList/>
    </div>
  </div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted } from 'vue'
import TaskList from './components/TaskList.vue'
import { useStore } from './store'
import { ActionTypes } from './store/actions'
export default defineComponent({
  components: { TaskList },
  setup() {
    const store = useStore()
    const loading = computed(() => store.state.loading)
    onMounted(() => store.dispatch(ActionTypes.GetTaskItems))
    const completedCount = computed(() => store.getters.completedTaskCount)
    const totalCount = computed(() => store.getters.totalTaskCount)
    return { loading, completedCount, totalCount }
  }
})
</script>
<style>
@import '~bulma/css/bulma.css';
</style>

Notice how we return loading, completedCount and totalCount. This is because we need to access them in the template. If we fail to return a method that is accessed in the template, Vue will trigger some errors.

TaskList

In order to launch the modal responsible for creating a task, we first need to create a method (setModal) that invokes the mutation that sets showCreateModal in the state to true.
We will create six computed properties:

  •  showCreateModal – that gets the showCreateModal property of the state.
  • showEditModal – that gets the showEditModal property of the state.
  • showTaskModal – that gets the showTaskModal property of the state.
  • tasks – that gets the list of tasks from the state.
  • showTaskId – that gets the showTaskId property of the state.
  • editTaskId – that gets the editTaskId property of the state.

Create TaskList.vue inside the components folder with the following code below:

    //components/TaskList.vue
    <template>
      <table class="table">
        <thead>
          <tr>
            <th><abbr title="Position">Task Id</abbr></th>
            <th>Completed</th>
            <th>Task</th>
            <th><abbr title="Won">Created By</abbr></th>
            <th><abbr title="Drawn">Assigned To</abbr></th>
            <th><abbr title="Lost">Actions</abbr></th>
          </tr>
        </thead>
        <tbody v-if="tasks">
          <tr v-for="task in tasks" :key="task.id">
            <TaskListItem v-bind="task" />
          </tr>
        </tbody>
        <tfoot>
          <CreateModal v-show="showCreateModal"></CreateModal>
          <button class="button is-link" @click="setModal">Create Task</button>
        </tfoot>
      </table>
      <EditModal v-if="showEditModal" :id="editTaskId"></EditModal>
      <TaskItem v-if="showTaskModal" :id="showTaskId"></TaskItem>
    </template>
    <script>
    import CreateModal from './CreateModal'
    import EditModal from './EditModal'
    import TaskItem from './TaskItem'
    import TaskListItem from './TaskListItem'
    import { defineComponent, computed } from 'vue'
    import { useStore } from '@/store'
    import { MutationType } from '@/store/mutations'
    export default defineComponent({
      name: 'TaskList',
      components: {
        CreateModal,
        TaskListItem,
        TaskItem,
        EditModal
      },
      setup() {
        const store = useStore()
        const setModal = () => {
          store.commit(MutationType.SetCreateModal, true)
        }
        const showCreateModal = computed(() => store.state.showCreateModal)
        const showEditModal = computed(() => store.state.showEditModal)
        const editTaskId = computed(() => store.state.editModalTaskId)
        const showTaskModal = computed(() => store.state.showTaskModal)
        const showTaskId = computed(()=> store.state.showTaskId)
        const tasks = computed(() => store.state.tasks)
    
        return { showCreateModal, setModal, tasks, showEditModal,showTaskModal, editTaskId, showTaskId }
      }
    })
    </script>
    <style scoped>  
    table {
      width: 100%;
    }
    .fa {
      font-size: 1.2rem;
      margin-left: 15px;
    }
    .fa:hover {
      font-size: 1.4rem;
    }
    </style>

Here the TaskList component houses the CreateModal, TaskListItem, TaskItem, and the EditModal components. It passes edidtaskId and showtaskId as props to EditModal and TaskItem respectively. We use the v-for directive to render the TaskListItem component for every task in the computed tasks property. We’ve passed each task as a task prop for every iterated TaskListItem component. We’ve used task.id as the unique key identifier. Our application will throw an error until we create all the children components in TaskList the component.

TaskListItem

This component receives the task props from every iteration in the TaskList component and renders the list of these task properties in the browser.

We will create four methods to mutate our store’s properties:

  • toggleCompletion – that toggles the `completed` property of a task.
  • removeTask – that commits the RemoveTask mutation (which removes a task from the state by its id ) with an id object as payload.
  • viewTask – that commits the SetTaskModal mutation (which allows us to view a single task).
  • editTask – that commits the SetEditModal mutation (which allows us to edit a single task).

Within the components folder, let’s create a new TaskListItem.vue file:

<template>
  <th>{{ id }}</th>
  <td>
    <input type="checkbox" :checked="completed" @change="toggleCompletion()" />
  </td>
  <td>{{ title }} <strong>(C)</strong></td>
  <td>{{ createdBy }}</td>
  <td>{{ assignedTo }}</td>
  <td>
    <span class="icon" @click="viewTask()">
      <i class="fa fa-eye"></i>
    </span>
    <span class="icon" @click="editTask()">
      <i class="fa fa-edit"></i>
    </span>
    <span class="icon" @click="removeTask()">
      <i class="fa fa-trash"></i>
    </span>
  </td>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import { MutationType } from '@/store/mutations'
export default defineComponent({
  props: {
    id: { type: Number, required: true },
    title: { type: String, required: true },
    createdBy: { type: String, required: true },
    assignedTo: { type: String, required: true },
    completed: { type: Boolean, required: true }
  },
  setup(props) {
    const store = useStore()

    const toggleCompletion = () => {
      store.commit(MutationType.CompleteTask, {
        id: props.id,
        completed: !props.completed
      })
    }
    
    const removeTask = () => {
      store.commit(MutationType.RemoveTask, {
        id: props.id
      })
    }
    const viewTask = () => {
      store.commit(MutationType.SetTaskModal, {showModal:true, taskId:props.id})
    }
    const editTask = () => {
      store.commit(MutationType.SetEditModal, {showModal:true, taskId: props.id})
    }
    return { toggleCompletion, removeTask, editTask, viewTask }
  }
})
</script>

TaskItem

This component receives the task id props from the TaskList component. The id props are used to fetch a task with the corresponding id via the getTaskById getters method and render these task properties in the browser.

We will create a computed property:

  • task – that calls the getTaskById getter method to retrieve a task by its Id

Also, we will create a method:

  • closeModal – that commits the SetTaskModal mutation which sets the showModal property in the state to false.

Within the components folder, let’s create a new TaskItem.vue file:

//components/TaskItem.vue
<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <h1>VIEW TASK</h1>
      <div class="card">
        <div class="card-content">
          <div class="media">
            <div class="media-content">
              <p class="title is-4">Title: {{ task.title }}</p>   
              <p class="subtitle is-6"> <b>Assigned To:</b>  {{ task.assignedTo }}</p>
              <p class="subtitle is-6"> <b>Created By:</b>  {{ task.createdBy }}</p>
            </div>
          </div>
          <div class="content">
            <p class="subtitle is-6">Description: {{ task.description }}</p>
          </div>
        </div>
      </div>
    </div>
    <button
      class="modal-close is-large"
      @click="closeModal"
      aria-label="close"
    ></button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue'
import { useStore } from '@/store'
import { TaskItem } from '@/store/state'
import { MutationType } from '@/store/mutations'
export default {
  name: 'EditModal',
  props: {
      id: { type: Number, required: true }
  },
  setup(props: any) {
    const store = useStore()
    const task = computed(() => store.getters.getTaskById(Number(props.id)))
    
    const closeModal = () => {
      store.commit(MutationType.SetTaskModal, {showModal :false, taskId: undefined})
    }
    return { closeModal, task  }
  }
}
</script>

CreateModal

This component is responsible for creating tasks.
Hence we will create two methods to mutate our state:

  • createTask – that commits the CreateTask mutation (which adds a new task to the tasks property in the state).
  • closeModal – that commits the SetTaskModal mutation (which sets showModal property in the state to false).

Within the components folder, let’s create a new CreateModal.vue file:

<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <form @submit.prevent="createTask">
        <div class="field">
          <label class="label">Task Title</label>
          <div class="control">
            <input
              v-model="title"
              class="input"
              type="text"
              placeholder="Enter task"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">Description</label>
          <div class="control">
            <textarea
              v-model="description"
              class="textarea"
              placeholder="Textarea"
            ></textarea>
          </div>
        </div>
        <div class="field">
          <label class="label">Assigned By</label>
          <div class="control">
            <input
              v-model="createdBy"
              class="input"
              type="text"
              placeholder="Enter Assigner's name"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">Assigned To</label>
          <div class="control">
            <input
              v-model="assignedTo"
              class="input"
              type="text"
              placeholder="Enter task creator's name"
            />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button type="submit" class="button is-link">Submit</button>
          </div>
          <div class="control" @click="closeModal">
            <button class="button is-link is-light">Cancel</button>
          </div>
        </div>
      </form>
    </div>
    <button
      class="modal-close is-large"
      @click="closeModal"
      aria-label="close"
    ></button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { useStore } from '@/store'
import { TaskItem } from '@/store/state'
import { MutationType } from '@/store/mutations'
export default {
  name: 'CreateModal',
  setup() {
    const state = reactive({
      title: '',
      description: '',
      createdBy: '',
      assignedTo: ''
    })
    const store = useStore()
    const createTask = () => {
      if (
        state.title === '' ||
        state.description === '' ||
        state.createdBy === '' ||
        state.assignedTo === ''
      )
        return
      const task: TaskItem = {
        id: Date.now(),
        title: state.title,
        description: state.description,
        createdBy: state.createdBy,
        assignedTo: state.assignedTo,
        completed: false,
        editing: false
      }
      store.commit(MutationType.CreateTask, task)
      state.title = ''
      state.createdBy = ''
      state.assignedTo = ''
      state.description = ''
    }
    const closeModal = () => {
      store.commit(MutationType.SetCreateModal, false)
    }
    return { closeModal, ...toRefs(state), createTask }
  }
}
</script>

Notice how we used vue3’s reactive method to store values that is used in the template.
We could create a variable as we normally do inside a setup function and add it to the returned object, then render it in the template. This will work but there will be no reactivity. Also we could use refs but then, it is used on primitives (strings, numbers, booleans).
When using reactive, we need to use toRefs to convert the reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.

EditModal

This component is responsible for updating tasks. Its logic is similar to the CreateTask component we just discussed above.
Hence we will create two methods to mutate our state:

  • updateTask – that commits the UpdateTask mutation (which modifies a task with a corresponding id in the task’s property of the state).
  • closeModal – that commits the SetTaskModal mutation (which sets showModal property in the state to false).

Within the components folder, let’s create a new EditModal.vue file:

<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <form @submit.prevent="updateTask">
          <h1>Edit Modal</h1>
        <div class="field">
          <label class="label">Task Title</label>
          <div class="control">
            <input
              v-model="title"
              class="input"
              type="text"
              placeholder="Enter task"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">Description</label>
          <div class="control">
            <textarea
              v-model="description"
              class="textarea"
              placeholder="Textarea"
            ></textarea>
          </div>
        </div>
        <div class="field">
          <label class="label">Assigned By</label>
          <div class="control">
            <input
              v-model="createdBy"
              class="input"
              type="text"
              placeholder="Enter Assigner's name"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">Assigned To</label>
          <div class="control">
            <input
              v-model="assignedTo"
              class="input"
              type="text"
              placeholder="Enter task creator's name"
            />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button type="submit" class="button is-link">Submit</button>
          </div>
          <div class="control">
            <button class="button is-link is-light">Cancel</button>
          </div>
        </div>
      </form>
    </div>
    <button
      class="modal-close is-large"
      @click="closeModal"
      aria-label="close"
    ></button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue'
import { useStore } from '@/store'
import { TaskItem } from '@/store/state'
import { MutationType } from '@/store/mutations'
export default {
  name: 'EditModal',
  props: {
      id: { type: Number, required: true }
  },
  setup(props: any) {
    const state = reactive({
      title: '',
      description: '',
      createdBy: '',
      assignedTo: ''
    })
    const store = useStore()
    const getTaskById = computed(() => store.getters.getTaskById(Number(props.id)))

    const setFields = () => {
       const task = store.getters.getTaskById(Number(props.id))
       if(task) {
           state.title = task.title
           state.createdBy = task.createdBy
           state.assignedTo = task.assignedTo
           state.description = task.description
       }
    }
    onMounted(() => { setFields() });
    const updateTask = () => {
      if (
        state.title === '' ||
        state.description === '' ||
        state.createdBy === '' ||
        state.assignedTo === ''
      )
        return
      const task: TaskItem = {
        id: props.id,
        title: state.title,
        description: state.description,
        createdBy: state.createdBy,
        assignedTo: state.assignedTo,
        completed: false,
        editing: false
      }
      store.commit(MutationType.UpdateTask, task)
      state.title = ''
      state.createdBy = ''
      state.assignedTo = ''
      state.description = ''
    }
    const closeModal = () => {
      store.commit(MutationType.SetEditModal, {showModal: false, taskId: undefined})
    }
    return { closeModal, ...toRefs(state), updateTask }
  }
}
</script>
<style scoped>
label {
    color: #ffffff;
}
h1 {
    color: #ffffff;
    text-align: center;
    font-size: 2rem;
    margin-bottom: 3rem;
}
</style>

In order to get the task properties already displayed in the form fields when the component is mounted, we had to call the setFields method on the onMounted lifecycle hook. The setFields triggers the getTaskById getters method to fetch a task from the state by its id, then update the properties in the reactive object with the fetched task properties.

Now you can run the application with:

npm run serve

Conclusion

We have used most of Vuex 4 major feature releases including its robust typing for the store and excellent integration with the Composition API in Vue 3 to build a dynamic task management application. We have practically explored the features of the Vue 3 composition API.

Also, we have seen how the createLogger function is used to log the mutations and state to the browser’s console making debugging the store easy even without the Vue developer tool.

I hope you have learned a great deal from this tutorial. Do reach out in the comment section below if you have any questions or suggestions.

Here is the github repo for the task management application.

Server-Side Rendering with Vue.js and Nuxt.js

Vue.js and Nuxt.js seamlessly complement each other for SSR. Vue.js provides the core functionality for dynamic UIs, while Nuxt.js enables server-side rendering of Vue.js components, ensuring fast initial page loads and optimal SEO. Nuxt.js simplifies SSR application development, allowing developers to focus on crafting exceptional user experiences without getting bogged down by SSR intricacies.

9 Replies to “Building Web Apps with Vue 3 composition API + Typescript + Vuex(4.0)”

  1. Very insightful article

    1. Emmanuel 4 years ago

      Thank you Janet. I am glad you found it useful.

  2. ql1993xl 4 years ago

    easy to understand, thanks

    1. Emmanuel 4 years ago

      Nice 👍

  3. Jan Junior 4 years ago

    Thanks for this great article. Is it possible to have different modules to?

    1. Emmanuel 4 years ago

      You’re welcome Jan! Yes it is possible

  4. This is an incredible and informative post, it’s practically a course by itself. Thanks a lot!

    1. Emmanuel 4 years ago

      You’re welcome Jean

  5. Very helpfull, thank you!
    One question: Do you see any problem in making the payload parameter in the ActionAugments type nullable?
    For one of my actions i dont need any payload and just want to fire a mutation but i get the error that payload wasnt provided.
    I can get rid of this error by doing “payload?: Parameters[1]” just wanted to confirm somehwere if this is the right way or if i might dont see something 🙂
    Thanks in advance!

Leave a Reply