是非、Vue.js

This is the English version of this article.

この記事の日本語版はこちらです。

I’ve been using Vue.js at work for a significant project over the last four months. During that time, I’ve come up with a shortlist of my least favorite elements of the framework.

The code in this article reflects the exact environment of our project. At a high level, that environment at the time of writing is as follows:

Table of Contents

  1. Motivation
  2. What is Vue?
  3. Problem: Template Type Safety
  4. Problem: Global Variables
  5. Problem: The Vuex Dilemma
  6. Problem: Inconsistent Reactivity
  7. Conclusions

Motivation

I currently work at a Unique Vision, a Tokyo-based software company. Every week, the engineers gather up and listen to each others’ tech talks. I originally wrote this article for one such tech talk.

I have one more motivation. Not all our projects use Vue, nor is it company policy to use it for new projects. As selecting a framework is an exceedingly difficult task, I’ve also tried to write an article that would be helpful when evaluating Vue for a project.

What is Vue?

Vue is a framework for creating re-usable components. Let’s define a component as the HTML and JavaScript required to use a single “thing”.

Of course, you don’t need Vue to do web development this way. What Vue brings to the table, is syntax sugar that trivializes component-based development.

Check out this example Vue component, a color picker:

<template>
  <div class="color-picker">
    <div 
      v-for="(color, index) in colors"
      :key="index"
      class="item"
      :class="color == value ? 'selected' : ''"
      :style="{ background: color; }"
      @click="$emit('input', color)"
    />
  </div>
</template>
import Vue from 'vue';
import { Component, Prop } from 'vue-class-component';

@Component
export default class ColorPicker extends Vue {

  @Prop({ required: true })
  colors!: string[];

  @Prop()
  value?: string;
}

Once you know Vue’s syntax, defining components is extremely concise. Using them is similarly brief. Here’s the exact code running the example:

<template>
  <div id="app">
    <color-picker v-model="color" :colors="colors" />
    <div>
      <span>Color: {{ color }}</span>
    </div>
  </div>
</template>
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import ColorPicker from '@/components/ColorPicker.vue';

const RED = '#ff0000';
const GREEN = '#00ff00';
const BLUE  = '#0000ff';

@Component({
  components: {
    ColorPicker,
  },
})
class App extends Vue {

  color: string = GREEN;

  get colors() {
    return [RED, GREEN, BLUE];
  }
}

In short, Vue’s syntax sugar lets you concisely define and consume components that do relatively complex things.

And less code means fewer bugs.

But at what price?

Problem: Template Type Safety

All Vue components declare a chunk of HTML with a <template> tag. Components use this HTML to pass arguments known as “properties” to their child components.

Sadly, despite a typed definition, properties passed via templates are not type-checked by TypeScript.

For example, our color picker defines its colors argument as a string array:

class ColorPicker extends Vue {

  @Prop({ required: true })
  colors!: string[];
}

However, if you mistakenly pass a non-string array:

<template>
  <div>
    <!-- Colors are also numbers...right? -->
    <color-picker 
      :colors="[
        0xff0000,
        0x00ff00,
        0x0000ff
      ]"
    />
  </div>
</template>

Your first error will be at runtime. To make matters worse, some native Vue components like <input> perform automatic string conversion, so you won’t even catch this issue until it manifests itself days later.

Our project tries to use the latest-and-greatest when it comes to type safety. However, due to the lack of types on <template>, we will never be able to close the loop completely.

If your project depends on eventually having complete type safety, Vue is not yet the framework for you.

Problem: Global Variables

The Vue template can only use data defined on the component. The color picker example defined a local getter in order to pass the globally defined colors to its template:

const RED = '#ff0000';
const GREEN = '#00ff00';
const BLUE  = '#0000ff';

@Component()
class App extends Vue {

  get colors() {
    return [RED, GREEN, BLUE];
  }
}

As a result, we were able to access a variable called colors in the template:

<template>
  <div class="color-picker">
    <!-- Fine: we defined `colors` locally. -->
    <div v-for="color in colors" />
  </div>
</template>

However, if you attempt to use the constants directly, you’ll find that they don’t exist:

<template>
  <div class="color-picker">
    <!-- Error: not defined, apparently. -->
    <div v-for="color in [RED, GREEN, BLUE]" />
  </div>
</template>

This inconvenience is exaggerated when you want to use utility libraries like lodash. You have no choice but to define the the functions locally, first.

(Although, if you put this together with the lack of type safety in templates, you may not want to do any processing in the template anymore, either.)

In our current project, there is a set of constants we call kbn shared between the client and server representing specific database constant values. As virtually every template uses them, we monkey-patch them into every component on construction:

import { VueConstructor } from 'vue';
import { KbnConstants } from '@/kbn';

export default {
  install(vue: VueConstructor) {
    vue.prototype.$kbn = KbnConstants;
  }
};

This comes with another catch - you have to satisfy TypeScript with a type declaration:

import Vue from 'vue';
import { KbnConstants } from '@/kbn';

declare module 'vue/types/vue' {
  interface Vue {
    $kbn: typeof KbnConstants;
  }
}

All this adds a overhead to the creation of every component and doesn’t scale. Moreover, it’s difficult to decide when to draw the line between defining common global variables locally, or just monkey-patching them in.

Bottom line: you will have to deal with passing globally defined constants and functions from components to templates, and it’s usually not pretty.

Problem: The Vuex Dilemma

In my opinion, some libraries are crippled by the lack of access to global variables.

One example is Vuex, a popular Vue library for managing state across components. You define your state and the operations over it in a central “store”, and interact with that state strictly through that store. It’s a useful abstraction, and many other frontend frameworks have a similar library.

The Vue version, however, is a little worse than imperfect. As an example, let’s use the previous color picker example, but spice it up by adding an enabled property on colors.

If the colors were defined in a Vuex module, it might look like this:

const store = new Vuex.Store({
  state: {
    colors: [
      { color: '#ff0000', enabled: true },
      { color: '#00ff00', enabled: false },
      { color: '#0000ff', enabled: true },
    ]
  },
  getters: {
    enabledColors: (state) => 
      state.colors.filter((color) => color.enabled),
  }
});

Great! Now all your components can look at one place, and use the same methods for interacting with the data. Except, look at what the Vuex documentation has you do in order to let you use that store in templates:

import store from '@/store';

@Component({
  computed: {
    enabledColors: store.getters.enabledColors,
  }
})
class App extends Vue {
  
}

You have to re-declare any variables and functions on the component, one by one.

We “solved” this by defining static, typed objects to access parts of the store. The objects are made available in every component, making them convenient to use - but are also static, making mock state for testing essentially impossible in the process.

If your project will eventually require programmatic UI testing, and you don’t want to be forced to choose between clean code and Vuex…

…then Vue is probably not the right framework for you.

Problem: Inconsistent Reactivity

In the live example above, you’ll notice that the text updates as soon as a color is selected. Vue generally claims to (efficiently) update the UI whenever any dependent variables are changed.

Unfortunately, there’s quite a bit of fine print to this claim.

For example, components can have “properties”, or variables passed to the component by its parents, and “data”, which are variables used for managing state local to the component.

Somehow, properties initialized as undefined are reactive, but data variables are not:

@Component
class App extends Vue {

  // Reactive.
  @Prop()
  property: string | undefined;

  // Not reactive.
  data: string | undefined;
}

If you plan on using an array in your template, some methods trigger a UI update, while others do not:

@Component
class App extends Vue {

  items: string[];

  // Reactive.
  update(index: number, item: string) {
    this.items.splice(index, 1, item); 
  }

  // Not reactive.
  update(index: number, item: string) {
    this.items[index] = item;
  }
}

Lastly, my personal favorite, Vue cannot detect property addition or deletion. Which is to say, only changes to properties that existed at initialization are made reactive:

interface Content {
  messages: string[];
  count: number;
}

@Component
class App extends Vue {

  content: Partial<Content> = {
    count: 0,
  };

  updateMessages(messages: string[]) {

    // Reactive.
    this.content.count = messages.length;

    // Not reactive.
    this.content.messages = messages;
  }
}

If you’re wondering, the official way to add or delete properties is to overwrite the entire content variable.

At the time of writing, Vue’s documentation justifies these issues by dumping the responsibility on “the limitations of modern JavaScript”.

To be blunt, if you’re not willing to memorize the various ways Vue is or is not reactive, stop while you’re ahead.

Conclusions

When you first learn the syntax to Vue templates and components, it seems expressive. But as a project grows and you have less control over it, things like type safety, automated UI testing, and DRY code become more important.

In my opinion, Vue is not ready for projects with those kinds of requirements.

However, if you’re looking for a lightning-fast way to iterate on designs in exchange for having to learn Vue’s quirks, then this may be the right choice for you.

Potpourri

The title is a Japanese pun that simultaneously translates to Let's use Vue.js! and Vue.js: Pros & Cons.