Building an application with Vue and TypeScript. Best practices, thoughts and recommendations.

2/17/2020 by Stefan Bauer
This post is over a year old. Some of this information may be out of date.

Some years ago, my team and I were tasked to create an embeddable checkout system for one of our applications. Angular, React and Vue were relatively new, and we did not have any prior experience with these frameworks. At this point, all of our apps were built with jQuery and RequireJS, and we knew that we definitely wanted to move away from that. I know that jQuery is still fine for simple CRUD apps and landing pages. However, a single page application (SPA) with complex state, routing, code splitting etc. is simply too much for it.

Early learnings

Back then, we chose Angular 2, because we wanted the safety and power that TypeScript offered. Angular’s boilerplates and style guide do an excellent job at teaching (and sometimes enforcing) best practices and solid patterns. However, Angular was still young and we soon felt the pain of any early adopter, namely breaking changes, an unstable ecosystem, few available articles and the confusion caused by the framework’s very name (SERPs were occupied by Angular 1). Even the way projects were compiled was far from unified at this point. Supporting ahead of time compilation (AOT) was really time consuming and frustrating.

Why Vue?

Around the same time, we used Vue as a drop-in replacement for jQuery in some of our backend views and small apps. Vue was very easy to use and expand, and has probably the best documentation I have seen so far. When using Vue, all of your prior SPA architecture learnings carry over. Vue was not ready for us at this moment, though. Most packages and articles were in Chinese, and that was a problem for us. However, when we started to build our new product comparison app about one and a half year later, we reconsidered all of the frameworks. All of them had matured quite a bit. We chose Vue over React and Angular at this point, because we were tired of Angular and had no real world experience with React. Furthermore, Vue’s TypeScript support seemed to be spot-on (which is the default for every framework by now, I guess).

We do not regret this decision. Slowly expanding a Vue application is as easy as it gets. However, having some prior experience with SPA design is highly recommended. Vue does not enforce folder, module or service structure as Angular does. Therefore, I think starting with Angular was the right decision for me personally. It tought me a lot about SPA architecture. There are many, very clever and experienced people working on the NG team. I think that it is thanks to Angular that I can really enjoy all of the flexibility, power and documentation Vue has to offer.

Best practices

Throughout the last years of using Vue (and Angular), we made quite some mistakes. We also did some things really well, I think. Here are some of the things we learned.

Setup

I recommend using Vue CLI to set up your project. The defaults are very reasonable and everything is clearly documented. It also makes it easy to upgrade your project later on.

Also, I think that VSCode is a great editor. However, Vue and TypeScript support in WebStorm has become so powerful that it's hard not to recommend it. Do yorself a favor and at least try WebStorm. It's worth the money.

You can find the Vue CLI documentation here, WebStorm here and VSCode here.

Think about code splitting before you build something

If you are building a large application, think about code splitting early on. Code on your server does not have this problem, but JavaScript apps are often huge append-only piles of code. Do not force your users to download all of your application, they will probably only need very small parts of it. Just as lazy loading perfectly sized images, code splitting should be one of your top priorities.

If you want to learn more about code splitting, check out Vue Router's lazy loading or this article on Medium.

Think about server requests before you build something

Most applications start out with one huge request that downloads all of the data from an API. You will regret this decision at some point, as such architectures become harder and harder to refactor and queries become a nightmare to segment and decouple. Only download data from a server as needed. If you are using GraphQL, make individual files for each query and store them in an extra folder.

Folder structure

Usually, my general folder structure looks like this:

.
├── public
   ├── fonts
   └── img
├── src
   ├── assets
   ├── components
   ├── feature
   └── shared
   ├── config
   ├── decorators
   ├── directives
   ├── enums
   ├── feature
   └── shared
   ├── filters
   ├── i18n
   ├── mixins
   ├── models
   ├── feature
   └── shared
   ├── plugins
   ├── queries
   ├── router
   ├── service-worker
   ├── services
   ├── store
   ├── commands
   └── module
   └── views
└── tests
    ├── e2e
    ├── helpers
    └── unit

Some general advice when it comes to structuring your Vue app is:

  • Always use local registration for components. You will want code splitting as your project grows, and this will make the transition much easier.
  • Use a shared and a feature folder. If you have to delete or redesign and entire feature, this will make your life much easier. New components should always start out as part of the feature folder. Move them to shared as needed.
  • Many developers call the views folder containers, especially when they are working with GraphQL. Do not be confused by that.
  • Make sub folders for your models and interfaces.
  • Also make sub folders for your enums.
  • Do not use mixins. But if you do, keep them in an extra folder.
  • Store global configuration in a config folder with sub folders, such as style or endpoints.
  • Use store modules. State tends to grow quickly and it is important to keep concepts separated.
  • Do not forget to type your store. This is a central part of your application and has to be robust.

Testing

This is not an article on testing, so I will keep this short and sweet. Do yourself a favor and write unit tests. Services, Vuex stores and components are rather easy to test and it saves you a lot of headache. I am not entirely sold on e2e tests, though. I wrote a whole bunch of them for the above-mentioned checkout system, and I did not find them useful at all. They were just time consuming. If you are interested in the pros and cons of e2e, others have far more sophisticated things to say about this.

TypeScript

Style

Treat yourself, use TypeScript. I mean, ultimately it is up to you, but TypeScript has improved the quality of all of our applications quite a bit and is pure joy to work with. Also, there are many great packages that make working with TypeScript even better.

Especially the packages Vue Class Component and Vue Property Decorator come highly recommended. Just have a look at this beautiful and expressive component. Read it from top to bottom and see, how quickly you can make sense of it.


<template>
  <div class="counter" @click="countUp">
    {{ title }}: <b>{{ count }}</b>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue';
  import {Prop} from 'vue-property-decorator';
  import Component from 'vue-class-component';


  @Component({name: 'my-counter'})
  export default class MyCounterComponent extends Vue {
    @Prop({required: false}) readonly initialCount!: number;
    @Prop({required: false, default: 'My Counter'}) readonly title!: string;

    private count: number = 0;

    private mounted() {
      if (Number.isInteger(this.initialCount)) {
        this.count = this.initialCount;
      }
    }

    private countUp() {
      this.count += 1;
    }
  }
</script>

<style scoped>
  .counter {
    padding: 10px 15px;
  }
</style>

Never use magic strings, use enums instead

Magic strings are a great way of introducing bugs into your application. They also make refactoring much harder. In Vanilla JS, we often tend to solve this problem with lots of screaming snake case constants. However, TypeScript's string enums are the perfect solution for most magic string related issues. They need less boilerplate, are nicer to read and are easily typed.

Compare this JavaScript example:

const PRODUCT_TYPE_PEN = 'PRODUCT_TYPE_PEN';
const PRODUCT_TYPE_PENCIL = 'PRODUCT_TYPE_PENCIL';
const PRODUCT_TYPE_PAPER = 'PRODUCT_TYPE_PAPER';

function getProductType(invoiceItem) {
    return invoiceItem.productType;
}

... with this TypeScript example:

enum ProductType {
    Pen = 'PEN',
    Pencil = 'PENCIL',
    Paper = 'PAPER',
}

function getProductType(item: InvoiceItem): ProductType {
    return item.productType;
}

Use "readonly" types

Make sure to use the readonly key word and readonly types such as ReadonlyArray<any>. Never again will mutated values ruin your day.

Type your store

I already mentioned that one, but it is important. If you do not type your store, refactoring will become hard and type errors will occur. The TypeScript support of Vuex will be greatly improved soon, and you should definitely make use of that.

State

Use Vuex

Many people will recommend doing your own thing, or using an event emitter for sharing state. However, Vuex is easy to debug, has a nice eco system around it and utilizes many well known conventions. If you want to make onboarding new team members easy, use patterns and packages that are sufficiently tested, documented and explored. Of course, it always depends on the size of your app and your exact use case, but Vuex comes highly recommended.

You can learn more about Vuex here.

Keep custom event names in an enum

Again, this is just repeating that you should not use magic strings. And you really shouldn't. Having an enum for events makes sure that you can always track event usage throughout your application.

enum CustomEvent {
    BookSelected = 'book.selected',
    BookRemoved = 'book.removed',
}

Avoid custom events

It is easy to produce bugs with events. From my personal experience, it is much easier to react to Vuex state than to work with custom events. If you find yourself relying on too many events, $nextTick calls (or even setTimeout calls), then you're probably heading towards desaster.

In case you don't know $nextTick yet, you can find its documentation here.

Avoid mixins (especially those that use lifecycle hooks)

They might seem like a good idea, but they just cause trouble. Even though a good package for mixins in Vue exists, mixins are hard to trace back, harder to debug and especially hard to justify. Usually, it makes more sense to write a component, directive or filter. If you think a mixin sounds like a good idea, do yourself a favour and try to find an alternative solution. It will save you a lot of headache in the long run.

This post gives you a good idea about why mixins are a bad idea. It is about React, but most of it is also true for Vue (and Angular).

Be mindful when working with arrays and objects

It is important that you understand/remember "by reference vs. by value" in JavaScript. Furthermore, it is also important to note that Vue cannot detect property addition or deletion due to technical limitations. When changing an object, do not use object['someNewProp'] = value. Use this.$set(object, 'someNewProp', value) ( or this.$set(object, 'someNewProp', undefined)) instead.

See Reactivity in Depth and "Javascript by reference vs. by value" for more details.

Vue

Avoid directives

Did you ever wonder whether you should use a component or a directive? In short, the answer is almost always "use a component". We have worked with directives in both Angular and Vue, and they have not contributed much. Instead, they were often a source of bugs and annoyance.

You can find an insightful StackOverflow answer to this question here.

Use lifecycle hooks and v-bind instead of vanilla event listeners

Use Vue's events and lifecycle hooks instead. It is easy to create race conditions or memory leaks with vanilla events when you're working with Vue (or Angular) and they don't work too well with $nextTick either. This is also one of the reasons why I do not recommend directives.

Use scoped CSS

It is a good idea to have general styles or frameworks linked in your App.vue without scope and to use scoped CSS for everything else. The CSS cascade is great and awful at the same time and using scopes lets you work around most issues people have with it.

You can learn more about scoped CSS in Vue here.

PS: If you like scoped CSS, you will also enjoy CSS contain.

Use BEM

Even if you scope your CSS, you should use BEM. BEM allows you to actually delete CSS with confidence and guarantees your stylesheets’ health. Methodologies like BEM are an industry standard for a reason. In one of my first SPAs I made the mistake to assume that you do not need BEM with scoped CSS. I was wrong. Deleting and refactoring styles has become quite atrocious in this project.

Check out BEM here. If you are not using it already, you should really consider starting right now.

Update: I wrote a little bit more about BEM. You can find this post here.

Wrap up

Building apps with Vue and TypeScript is fun, and you can create amazing user experiences with it. I hope this post helps you building even better apps while having even more fun.

Cheers.