Vue 3 composition API guide tutorial with snippet examples

October 14, 2020

What is the Vue 3 composition API?

Vue 1 and 2 both used the options API. If you have read any Vue tutorial in the last few years, you will be familiar with it (when you had the data() function, computed object with computed property functions, functions such as mounted(), unmounted() etc).

Vue 3 still fully supports the options API (and it is recommended to use for simple projects). But it has introduced a new way of doing things called the composition API.

(The new Vue composition API is quite similar to React hooks.)

A simple example of using the Vue composition API looks like this:

<template>
  <p>Hello {{ name }}</p>
  <button @click="changeName">change name</button>
</template>
<script>
import { ref, computed } from 'vue'; 
export default {
  setup() {
    const name = ref('Your Name');

    // computed property, calculating the number of characters
    // on load it will be 9 ('Your name'). Once the button is
    // pressed it will be 7 ('My name'):
    const nameLength = computed(() => name.value.length);

    function changeName() { 
      name.value = 'My name';
    }
    return { name, changeName, nameLength };
  }
};
</script>

Explanation of the composition API: The setup() function sets up a couple of variables (name and the changeName function). These are then returned near the end (return { name, changeName }).

The <template>...</template> section will be used during render - and the crucial part is that it has access to name, changeName, and nameLength (the variables that were returned in setup()).

What is ref() in Vue 3?

The ref() function (imported at the top of the script section: import { ref } from 'vue') is a function that will make a variable reactive.

(ref() should be used with primitives, such as numbers/strings. Read on to also find out about reactive())

You can imagine that name is structured like this:

{
  value: 'Your name'
}

This is why when the changeName() function wants to update the name, it must set `name.value = 'My name'.

(Updating just name = 'A new name' would not work.)

One of the magic things that Vue 3 does is it lets you access name directly in the <template> section.

(Notice that you don't need to do Hello {{ name.value }} - instead you can write Hello {{ name }}. This is handled automatically by Vue's templating system.

How to update ref() values

Always remember to set the .value attribute, or it will not work.

    let yourCounter = ref(1234); // or const!
    // correct ways to update:
    yourCounter.value++;
    yourCounter.value = 456;

    // incorrect ways:
    yourCounter++;
    yourCounter = 789;

Vue 3's computed() function explanation

A line from the example above that may look familiar to Vue 2 developers is the computed() function.

This is very similar to the computed object in the options API. This is how the computed property would look in the options API in Vue 2:

{
// ...
  computed: {
    nameLength() {
        return this.name.length
    },
    // ...
  }
}

But in Vue 3's composition API lets you set up your computed properties in the setup() function.

Here is the computed code again, written in a slightly different format:

    const name = ref('Something');
    function calculateNameLength() {
        return name.value.length;
    }
    const nameLength = computed(calculateNameLength);
 
    // Note: like ref(), you must use .value with computed
    // values within setup()
    const isNameLengthLong = computed(() => nameLength.value.length > 5)

The calculateNameLength function will be run:

  1. initially run straight away, and it will return the length of 'Something' (which is 9).
  2. And then again in the future any time that the `name` value is updated.
Whatever value is returned from the function passed to `computed` (initially `9`) will be the value that `nameLength` is set to.

Note: if you need to access a computed property value in setup(), then you must use .value.

If you need to access a computed property within <template>, you do not need to add .value as it is automatically unwrapped by Vue 3.

How to use Vue 3's watch() and watchEffect() functions

In Vue 2 (or Vue 3 using the options API) you have probably used the watch feature.

It looked something like this

{ // ...
  data() {
    return { yourCounter: 0, anotherCounter: 100 }
  },
  methods: {
     incrementCount() {
         this.yourCounter++;
     }
  },
  watch: {
      yourCounter(newValue, prevValue) {
          console.log("this.yourCounter was just updated!");
          console.log(newValue)
          console.log(prevValue)
          this.anotherCounter--;
      }
  },
// ... }

Every time this.yourCounter is mutated, it will run the function yourCounter() which will console.log() some output and decrement this.anotherCounter.

In Vue 3 there are a couple of ways to do the same thing. The easiest is with watchEffect() but you can also use watch().

I'll start with watchEffect():

import { watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => console.log(count.value))
// initially logs 0

setTimeout(() => {
  count.value++
  // logs logs 1
}, 100)

watchEffect() will initially run the function you pass to it (() => console.log(count.value)). It will then track all dependencies (in this case count was accessed, so that is a dependency).

After initially running, whenever count is updated it will rerun the function again.

And how to use watch() in Vue 3:

const count = ref(0)
watch(count, (value, prevValue) => {
   console.log('Count was updated');
   console.log({value, prevValue});   
})

watch() is similar to watchEffect(), but has a bit more code to set it up. It will also only watch the value variable passed in the first argument.

When to use watch() instead of watchEffect():
  • Watch it lazily (do not immediately run the function)
  • Only re-run the function when a specific variable was updated
  • Have access to the current (new) value and the previous value

For most typical uses you can normally get away with using watchEffect.

How to use template refs in Vue 3

In Vue 2 (or Vue 3 with the options API) you could use this.$refs.something to access a template ref (such as <button ref="something">...</button>.

In Vue 3's composition API you cannot use this in setup().

This is how you use refs in Vue 3 with setup():

<template>
  <button ref="btnRef">Click me</button>
</template>

<script>
import { ref, onMounted } from 'vue';
export default {
    setup() {
        const btnRef = ref(null);

        onMounted(() => {
          console.log(btnRef.innerHTML);
          // logs the HTML of the <button>
        });

        return { btnRef }
    }
}
</script>

To use refs in Vue 3 you must import ref, then create a variable from ref(null). Pass that to the object that setup() references, and use the same variable name as the ref in the HTML.

How to use <script setup> in Vue 3

The Vue 3 script setup syntax is syntactical sugar that will make writing Vue 3 code a bit easier, and it ultimately compiles to standard Vue 3 code.

As well as export default { ... } (like most examples of Vue 3 on this page), you can also use the Vue 3 script setup way of doing it:

<template>
  <h1>{{ person.name }}</h1>
  <h2>{{ person.age }}</h2>

  <button @click="incrementAge">+1</button>
</template>
<script setup>
import { reactive } from 'vue';

export const person = reactive({
  name: "webdevetc",
  age: 99
})

export const incrementAge = () => person.age++
</script>

Using the <script setup> syntax you can just export all of your variables, without having to export an object.

For more advanced use of <script setup> see here.

<script setup="props">
import { watchEffect } from 'vue'

watchEffect(() => console.log(props.yourProp))
</script>

What is so special about setup()?

The setup() function is new to Vue 3. This is where you can combine all of your data properties, computed properties and much more.

There are a few important things to know about setup():

  • You can use imported variables and functions (I will show some examples later) - this means you can easily share code between components. Before Vue 3 it was messy to do this (e.g. by mixins)
  • setup() is run before any of the normal lifecycle hooks.

What lifecycle hooks are available in Vue 3's setup() function?

You have access to most of Vue 3's life cycle hooks in setup(). Here is an example:

import {
  onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate,
  onDeactivated, onErrorCaptured, onMounted, onUnmounted, onUpdated,
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => console.log('before mount'));
    onMounted(() => console.log('mounted'));
    onBeforeUpdate(() => console.log('before update'));
    onUpdated(() => console.log('updated'));
    onBeforeUnmount(() => console.log('before unmount'));
    onUnmounted(() => console.log('unmounted'));
    onActivated(() => console.log('activated'));
    onDeactivated(() => console.log('deactivated'));
    onErrorCaptured(() => console.error('error captured'));
    onRenderTracked(() => console.log('render tracked'));
    onRenderTriggered(() => console.log('render triggered'));
  }
};

Note: setup() is called between beforeCreate() and created(), so there is no need to use them. Just put whatever code you would need in those functions in setup() itself.

Making reactive variables in Vue 3 with composition API

If you have been following along, you have already seen ref().

With ref() any changes to it within setup() must use the .value syntax:

 {
    setup() {
        const yourAge = ref(30);
        // notice to increment it you must 
        // update `yourAge.value`:
        const incrementAge = () => yourAge.value++
       
        return { yourAge, incrementAge }
    }
 }

(Within your <template> block you can use it without .value as Vue will automatically unwrap it. )

ref() should be used with primitive data types (undefined, Boolean, Number, String, BigInt, Symbol).

But for non primitive data types (which in 99% of the time will be Object) you should use reactive().

TODO - explain the data structure of ref()

Using reactive() with objects in Vue 3 composition API

If you want to work with some reactive data is not a primative (such as objects) then you should use reactive().

(reactive() is similar to Vue 2's Vue.observe())

You cannot use reactive() with primitives, it won't work

Unlike ref() which makes you access .value within setup, you can access it as if it was a regular object:

 import { reactive } from 'vue';

 {
    setup(){
      const person = reactive({
        age: 30,
        nationality: 'British',
      });
 
      const personIsAdult = computed(() => person.age >= 18)
  
      return { person, personIsAdult }
    }
 }

Read more about Vue 3

Read more about Vue 3 and how to use Vue 3 here.

TODO - more coming soon