Skip to the content.

10 Mistakes to Avoid When Starting with Vue 3

TL;DR:

Vue 3 has been stable for quite some time now. Many codebases are using it in production, and everyone else will have to migrate eventually. I had the opportunity to work with it and kept track of my mistakes, which you probably want to avoid.

1. Using the Reactive Helper for Declaring Primitives

Data declaration used to be straightforward, but now multiple helpers are available. The general rule now is:

Using reactive for a primitive will result in a warning, and the value will not be made reactive.

/* DOES NOT WORK AS EXPECTED */
<script setup>import {reactive} from "vue"; const count = reactive(0);</script>

2. Destructuring a Reactive value

Let’s imagine you have a reactive object with a counter and a button to increase it.

<template>
  Counter: 
  <button @click="add">Increase</button>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const state = reactive({ count: 0 });

    function add() {
      state.count++;
    }

    return {
      state,
      add,
    };
  },
};
</script>

Pretty straightforward and works as expected, but you might get tempted to leverage JavaScript destructuring and do the following.

/* DOES NOT WORK AS EXPECTED */
<template>
  <div>Counter: 8</div>
  <button @click="add">Increase</button>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const state = reactive({ count: 0 });

    function add() {
      state.count++;
    }

    return {
      ...state,
      add,
    };
  },
};
</script>

Code looks the same and, based on our previous experience, should work, but in fact, Vue’s reactivity tracking works over property access. This means we can’t assign or destructure a reactive object because the reactivity connection to the first reference is lost. This is one of the limitations of using the reactive helper.

3. Getting Confused with .value

On a similar note, one of the quirks of using ref can be hard to get used to.

Ref takes a value and returns a reactive object. The value is available inside the object under the .value property.

const count = ref(0);

console.log(count); // { value: 0 }
console.log(count.value); // 0

count.value++;
console.log(count.value); // 1

But refs are unwrapped while used in the template, and the .value is not needed.

<script setup>
import { ref } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}
</script>
<template>
  <button @click="increment">
    8
    <!-- no .value needed -->
  </button>
</template>

But be careful! Unwrapping only works on top-level properties. The following snippet will produce [object Object].

// DON'T DO THIS
<script setup>
import { ref } from 'vue';

const object = { foo: ref(1) };
</script>
<template>
  
  <!-- [object Object] -->
</template>

Using .value correctly requires time. Although I occasionally forgot about it, I first found myself using it more frequently than needed.

4. Emitted Events

Since the initial release of Vue, a child component can communicate with the parent using emits. You only needed to add a custom listener to listen for an event.

this.$emit("my-event");
<my-component @my-event="doSomething" />

Now emits need to be declared using the defineEmits macro.

<script setup>const emit = defineEmits(['my-event']); emit('my-event');</script>

Another thing to keep in mind is that neither defineEmits nor defineProps (used to declare props) need to be imported. They are automatically available when using script setup.

<script setup>
const props = defineProps({
  foo: String
});

const emit = defineEmits(['change', 'delete']);
// setup code
</script>

Lastly, because the events now have to be declared, the usage of the .native modifier is not needed, and it’s, in fact, been removed.

5. Declaring Additional Options

There are a few properties of the Options API method that are not supported from the script setup:

The solution is to have 2 different scripts in the same component as defined in the script setup RFC.

<script>
  export default {
    name: 'CustomName',
    inheritAttrs: false,
    customOptions: {}
  }
</script>
<script setup>// script setup logic</script>

6. Using Reactivity Transform

Reactivity Transform was one experimental but controversial feature of Vue 3 that was aiming to simplify the way of declaring a component. The idea was to leverage compile-time transforms to automatically unwrap a ref and make .value obsolete. But it is now dropped and will be removed in Vue 3.3. It will be still available as a package, but since it’s not part of the Vue core, it’s better not to invest time in it.

7. Defining Async Components

Async components were formerly declared by enclosing them in a function.

const asyncModal = () => import("./Modal.vue");

Since Vue 3, async components need to be explicitly defined with the defineAsyncComponent helper.

import { defineAsyncComponent } from "vue";

const asyncModal = defineAsyncComponent(() => import("./Modal.vue"));

8. Using Unnecessary Wrappers in Templates

A single root element for the component template was required in Vue 2, which sometimes introduced unnecessary wrappers.

<!-- Layout.vue -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

This is no longer the case, as multiple root elements are now supported. 🥳

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

9. Using the Wrong Lifecycle Event

All of the component lifecycle events got renamed, either by adding the on prefix or by changing name completely. You can check all the changes in the following graphic.

Lifecycle Events

10. Skipping the Documentation

Lastly, the official documentation has been revamped to reflect the new APIs and include many valuable notes, guides, and best practices. Even if you are a seasoned Vue 2 engineer, you will definitely learn something new by reading it.

Conclusion

Every framework has a learning curve, and Vue 3’s is unquestionably steeper than Vue 2’s. The composition API is much cleaner and feels natural after you get the hang of it.

Making mistakes is a lot better than not doing anything.

Ref: fadamakis.com