michaelAwad.io

Using Nuxt with the Vue 3 Class Based Components in combination with TypeScript

...

April 13, 2021

Nuxt is a framework based upon VueJS and is getting popular, and there is a good reason for that, which I will be covering in this article. In this article, I want to combine Nuxt with the latest techniques, such as the VueJS 3 class-based components. Without further ado, let’s dive into it!

What is Nuxt

With Nuxt, you can generate a project that already has the optimal setup and supports some great features out of the box:

  • Server-side rendering or Static Site Generation
  • File System Routing based on pages folder directory
  • Hooks for async data fetching
  • Adding Meta tags/SEO settings using a function
  • Config file for global setup

In general, it improves the quality of life and saves you a lot of overhead and architectural decisions you don’t feel like thinking about when you just want to focus on bringing your idea to life.

What is the VueJS Class-based component?

When you usually set up a VueJS component, you place everything in the respective lifecycle hooks. This is what we call the options API

For this article, we are going to use modern technologies, so we can’t ignore TypeScript. That is why we are going to work with VueJS class-based components. Its syntax is cleaner, mixins are no longer needed to separate concerns, for example, business logic from templates. Also, it makes it easier to share that business logic between components.

Setup the Project

As mentioned earlier with Nuxt, you can generate your app so let’s start with exactly that. Go to the directory where your app should live and enter the following command.

yarn create nuxt-app <app-name>

You will be asked several questions; these options are the relevant ones for the things we are covering in this article:

  • Select TypeScrip as a programming language
  • Axios - Promise based for NuxtJS Modules

Nuxt CLI Nuxt CLI

Go to the app directory, where we have to install a dependency for using TypeScript.

npm install --save-dev @nuxt/typescript-build

For our app to run, we need to add this to our config file as well. So in the root of your project, go to the nuxt.config.js file and add the following to the buildModules array if not already there:

"@nuxt/typescript-build"

Do the same for the modules array and check if the following is in there:

"@nuxtjs/axios"

To access the store’s data using Axios, we need to create our own Nuxt Axios instance.

  • Step 1: Create a utils folder in the root and add a file called api.ts and fill it with the following code:
import { NuxtAxiosInstance } from "@nuxtjs/axios"

// eslint-disable-next-line import/no-mutable-exports
let $axios: NuxtAxiosInstance

export function initializeAxios(axiosInstance: NuxtAxiosInstance) {
  $axios = axiosInstance
}

export { $axios }
  • Step 2: Create the Nuxt plugin. Create a plugins folder in the root containing a file named axios-accessor.ts and fill it with the following code:
import { Plugin } from "@nuxt/types"
import { initializeAxios } from "~/utils/api"

const accessor: Plugin = ({ $axios }) => {
  initializeAxios($axios)
}

export default accessor
  • Step 3: Add our plugin to the nuxt.config.js file. Add the reference to the axios-accessor.ts file using adding the following to the plugins array:
 '~/plugins/axios-accessor.ts'

The last thing we need to do for the setup of our Nuxt class-based Typescript app is to add a tsconfig.json file to root containing the following code:

{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/_": [
"./_"
],
"@/_": [
"./_"
]
},
"types": [
"@nuxt/types",
"@types/node",
"@nuxtjs/axios"
]
},
"exclude": [
"node_modules",
".nuxt",
"dist"
]
}

Creating our first component using the VueJS Class-Based API

The Vue Options API is the standard way to use VueJS. For this, there are no additional packages needed. You can start with this of the bat using the standard lifecycle hooks: data, mounted, computed, etc.

We need to install the Nuxt version of the property decorators, which are required to work with the Vue 3 class-based API

npm install --save nuxt-property-decorator

Let’s initialize our first component:

//Typescript code
<script lang="ts">
  import {(Component, Vue)} from 'nuxt-property-decorator' @Component export
  default class LearningModule extends Vue {}
</script>

In this code block, you see that we added lang=ts within the first script tag, and in the import. We import Component and Vue from the nuxt-property-decorator. This is the basic setup of a component; later in this article, we will make our component use data from a Vuex store.

Connect to an API using a Vuex Store

To make this exercise even more fun, we are creating a class-based Vuex store in Nuxt. For now, it will only be able to retrieve data from an API.

For this, we need vuex-module-decorators, so let’s install those:

npm install -D vuex-module-decorators

Create a file in the store folder named learningModules.ts and fill it with the following code :

import { Module, VuexModule, Action, Mutation } from "vuex-module-decorators"
import { $axios } from "~/utils/api"
import { ILearningModules } from "~/types/LearningModules"

@Module({
  stateFactory: true,
  namespaced: true,
})
export default class getLearningModule extends VuexModule {
  learningModules: ILearningModules[] = []

  @Mutation
  public async setLearningModules(learningModules: ILearningModules[]) {
    this.learningModules = learningModules
  }

  @Action({ rawError: true })
  public async getLearningModules() {
    await $axios
      .$get("http://localhost:3030/learning-modules")
      .then(payload => {
        this.context.commit(
          "setLearningModules",
          payload.data as ILearningModules[]
        )
      })
  }
}

We can recognize the decorators using the @ sign in front of them. Two properties need to be set; you can see those in the @Module decorator.

A common mistake here is to pass this into a module:

{
  name: "learningModules"
}

This will cause the following error:

Uncaught (in promise) Error: ERRSTORENOT_PROVIDED: To use getModule(), either the module* should be decorated with store in decorator, i.e. @Module({store: store}) or store should be passed when calling getModule(), i.e. getModule(MyModule, this.$store)

The solution to this issue is to remove the name prop altogether from the @Module property decorator; the filename takes care of this when you set namespaced to true. The documentation is not very clear about this, and also the solution to this error is not easily found on Google. You will end up eventually with a class-based component and a regular Vuex store. Which is suboptimal, confusing, and inconsistent. Unfortunately, I see this in several articles with regards to this topic.

The Module property decorator should look like this:

@Module({
stateFactory: true,
namespaced: true,
})

Since we are using TypeScript, make sure you create an interface to handle the data we get back from the API. In this case, create a types folder in the root with a file named: learningModules.ts and fill it with the following code:

export interface ILearningModules {
  title: string
  chapters: IChapters
  summary: string
  thumbnail: string
  started: boolean
  free: boolean
  users: IUsers
}

export interface IChapters {
  [index: number]: {
    title: string
    description: string
    vimeoId: string
    started: boolean
    free: boolean
    completed: boolean
  }
}

export interface IUsers {
  [index: number]: {
    userId: string
    progress: string
  }
}

Now you can create a data property for the initial state and type it:

learningModules: ILearningModules[] = []

Under the @Mutation decorator, we create the function that mutates the state and sets it with the payload from the @Action decorator we will create.

Under the @Action decorator, we create the function that uses the @Mutation decorator to set the state with the API’s data. In this function, we use the Axios util we created earlier to make an HTTP call to an API. In this case, the FeathersJS API from this article: Building a TypeScript NodeJS backend API using FeathersJS

When there is a successful response we use the following syntax to trigger the mutation with the payload as an argument. We type the payload.data argument with the Interface we created to make sure the data we receive is the same as the data we expect to get:

await $axios.$get("http://localhost:3030/learning-modules").then(payload => {
  this.context.commit("setLearningModules", payload.data as ILearningModules[])
})

Using the data from the Vuex Store

Now that we created our store, it is time to utilize it in our component. Since we used a namespaced Vuex module, we can do the following: Import the namespace from the property decorator

import { Component, Vue, Emit, namespace } from "nuxt-property-decorator";

Call the namespace and assign it to a variable for ease of use:

const learningModules = namespace("learningModules");

Where your data properties are, you can use de property decorator syntax to access the Vuex Store. Now below the property decorator, you can add the data property. Make sure to make it public, so the Vuex store has access. Also, this property can’t be undefined or null. Therefore we add the bang operator behind it, this is even required when TypeScript compiler is set to strict.

 @learningModules.State
  public learningModules!: [];

Now in the component, we can use this data property like we normally do.

The same goes for the Action function we created to get the data from the API. We again can use the property decorator syntax; only this time, instead of a data property, we make it a function. Why? You may ask. This is because in the Vuex store the Mutation we created is also a function. The same principles apply; we make it public and add the bang operator behind it.

  @learningModules.Action
  public getLearningModules!: () => ILearningModules[];

When everything is working correctly, you should be able to see something similar in your Vue Dev Tools:

Vue Dev Tools State Vue Dev Tools State

Let’s use the data in the template. When we checked the state, we saw the learningModules array. So all we have to do in the template is loop over it.

<div v-for="module in learningModules" :key="module._id">
  <h1>{{ module.title }}</h1>
</div>

Also in the template we can use it like a normal data property!

Conclusion

We covered what Nuxt is and the quality of life improvement with regards to saving energy on thinking of the overhead and architectural decisions. We used the latest techniques to create the app; Vue class-based components with TypeScript. Instead of the classic Vue option API.

We covered how to set up the Vuex store and the class-based property decorators and use that property decorator notation inside a template to make use of the Vuex store. Removing an abundance of steps and code required in the original options API

This approach requires a particular config and notation for it to work correctly, but it works like a charm when set up. Please be very specific and precise with the setup when using this workflow, following this article’s exact steps.

Happy Coding!


Michael Awad

I'm fascinated by the power of a strong mindset. I combine this with being a web developer, which keeps me motivated. But how, you may ask? That's what I share on this website. For more information about me personally, check out the about me page: About Me

Are you enjoying this blog and do you want to receive these articles straight into your inbox? Enter your e-mail below and hit the subscribe button!



I won't send you spam and you can unsubscribe at any time