Vue.js 3 Composition API Basics Tutorial

In this Vue tutorial we learn a different way to write component logic with the Composition API.

We cover how to move to the new setup option and how to replace options from the Options API with their new function counterparts.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI .

What is the Composition API?

As mentioned in the Vue API lesson , the Composition API combines all of a component’s data and functionality into a single section.

Instead of separating our code into different options like data , methods , computed etc. we now use only the setup option.

The setup option

The Composition API’s only focus is changing how the data, methods, watchers and computed options of a component can be written differently. Instead of using separate options, they are now bundled into a new setup option.

The option takes a function as a value, so we can use the ES6 shorthand syntax and simply write it as setup() .

Syntax: setup option
<script>
export default {
  setup() {

  }
}
</script>

Vue executes the function before the component is created, before any lifecycle hooks .

As an example, let’s log a message to the console in setup , as well as the created and mounted hooks.

Example: src/App.vue
<template>
  <div></div>
</template>

<script>
export default {
  setup() {
    console.log('Setup')
  },
  created() {
    console.log('Created')
  },
  mounted() {
    console.log('Mounted')
  }
}
</script>

The console output from the example above shows the order of execution.

Output: order of execution
Setup
Created
Mounted

note Because setup is called so early in the lifecycle, the component hasn’t been initialized yet and this doesn’t reference the config object like we’re used to. We don’t use this in the setup option.

The setup option takes two optional arguments.

  • props . Data that’s passed down from a parent component.
  • context . A Javascript object that exposes 3 different component properties. Namely attributes , slots and emit events.
Syntax: arguments
<script>
export default {
  setup(props, context) {
    // access props, attributes
    // slots and emit events
  }
}
</script>

To access data from the function in the components template, we return that data at the end of the setup option as an object (like we did with the data option).

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
export default {
  setup() {

    const greeting = "Hello there"

    return {
      greeting: greeting
    }
  }
}
</script>

When a key has the same name as its value, we can use the ES6 shorthand syntax.

Syntax:
return {
  greeting
}

note The data we returned above is not automatically reactive like it is in the data option. We have to manually do it with reactive refs.

Replacing the data option with ref()

If we want to make our returned data reactive, we use the ref function.

The function is imported from the ‘vue’ package and takes the data we want to make reactive as an argument.

Syntax: ref
setup() {
  // define ref
  const refName = ref(value)
}

If we want the data to be available in the template, we have to return the ref from the setup option (like we do with the data option).

Syntax: returned ref
setup() {
  // define ref
  const refName = ref(value)

  // make available to
  // the template
  return { refName }
}

If we want to access the value of a ref inside the setup option, we have to use the value property.

Syntax: ref.value
setup() {
  // define ref
  const refName = ref(value)

  // access ref value
  refName.value
}

As an example, let’s create a ref with a simple greeting message that we return and output in the template. We’ll also access the value in a timer inside setup and change its value after 3 seconds.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
import { ref } from 'vue'
import { setTimeout } from 'timers';

export default {
  setup() {
    // define ref
    const greeting = ref('Hello World')

    setTimeout(() => {
      // change ref 'value'
      greeting.value = 'Hello there'
    }, 3000)

    // return for use in
    // the template
    return { greeting }
  }
}
</script>

A ref doesn’t have to be a primitive value, like a string or a number. We can use objects as well.

If we want to access it inside setup , we have to use the value property again, followed by the object’s key.

Example: src/App.vue
<template>
  <p>{{ greeting.msg }} {{ greeting.user }}</p>
</template>

<script>
import { ref } from 'vue'
import { setTimeout } from 'timers'

export default {
  setup() {

    const greeting = ref({ msg: "Hello", user: "John" })

    setTimeout(() => {

      greeting.value.msg  = 'Greetings'
      greeting.value.user = 'Jane'
    }, 3000)

    return { greeting }
  }
}
</script>

If we expose the object’s properties outside of the setup option, they lose their reactivity. They will display fine in the template, but they won’t update when the timer executes.

Example: src/App.vue
<template>
  <p>{{ greetingMsg }} {{ greetingUser }}</p>
</template>

<script>
import { ref } from 'vue'
import { setTimeout } from 'timers'

export default {
  setup() {

    const greeting = ref({ msg: "Hello", user: "John" })

    setTimeout(() => {

      greeting.value.msg  = 'Greetings'
      greeting.value.user = 'Jane'
    }, 3000)

    return {
      greetingMsg:  greeting.value.msg,
      greetingUser: greeting.value.user
    }
  }
}
</script>

We have to expose the whole object, not the separate properties.

As mentioned earlier, ref creates a wrapper object when we pass single primitive values to it.

In Javascript, primitives are passed by value, not by reference. Wrapping values in an object helps keep the behavior unified across different data types. So having them in an object allows us to safely pass it across the app without having to worry that the data will lose its reactivity.

Reactive data with reactive()

The reactive function is like ref but it can only take objects, not primitives.

One benefit of reactive is that we don’t need to use the value property to access the object’s value.

Example: src/App.vue
<template>
  <p>{{ greeting.msg }} {{ greeting.user }}</p>
</template>

<script>
import { reactive } from 'vue'
import { setTimeout } from 'timers'

export default {
  setup() {

    const greeting = reactive({ msg: "Hello", user: "John" })

    setTimeout(() => {
      // no need for the 'value' property
      greeting.msg  = 'Greetings'
      greeting.user = 'Jane'
    }, 3000)

    return {
      greeting
    }
  }
}
</script>

But ref has the value property for reassignment, reactive doesn’t and therefore cannot be reassigned.

ref vs reactive: When to use which?

As we mentioned earlier, reactive can only receive objects and cannot be reassigned so it’s use is limited. But ref calls reactive in the background, so in some cases you might want to avoid the overhead from the extra step and use reactive instead of ref .

Use ref

  • When you have primitive values (number, boolean, string etc.).
  • When you have an object you need to reassign later (like swapping an array for another).

Use reactive

  • When you have an object you won’t need to reassign and you want to avoid overhead of ref .

We use ref exclusively simply because sometimes using value and sometimes not can be confusing. We prefer the consistency of always using ref .

Many other developers also prefer only using ref, and some even consider reactive to be harmful .

Replacing the methods option with functions

With the Options API we define custom functions in an option called methods .

Example: methods option
methods: {
  functionName() {

    // do something
    return something
  }
}

With the Composition API, we define functions directly in setup .

The functions can be regular Javascript functions, anonymous functions or arrow functions.

Like data, we have to return a function from the setup option if we want it to be available in the template.

Syntax: functions
setup() {
  // regular function
  function funcOne() {}

  // anonymous function
  const funcTwo = function() {}

  // arrow function
  const funcThree = () => {}
}

As an example, let’s create three functions that change the value of a ref when the button they’re bound to is clicked.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
  <p><button @click="newGreeting1">Regular function</button></p>
  <p><button @click="newGreeting2">Anonymous function</button></p>
  <p><button @click="newGreeting3">Arrow function</button></p>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {

    const greeting = ref('Hello World')

    // regular function
    function newGreeting1() {
      greeting.value = 'Hello John'
    }

    // anonymous function
    const newGreeting2 = function() {
      greeting.value = 'Hello Jane'
    }

    // arrow function
    const newGreeting3 = () => {
      greeting.value = 'Hello Jack'
    }

    return {
      greeting,
      newGreeting1,
      newGreeting2,
      newGreeting3
    }
  }
}
</script>

Replacing the computed option with computed()

With the Options API we define computed properties in an option called computed .

Example: computed option
computed: {
  computedFunction() {

    return computed_value
  }
}

With the Composition API, we define computed properties with the computed function. The function is imported from the ‘vue’ package and takes a callback function as an argument.

Syntax: computed
setup() {
  // computed function
  const computedName = computed(() => {})
}

As an example, let’s take a first and last name from inputs in the template, then combine them in the computed function and output it to the template.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
  <input type="text" placeholder="First Name" @input="setFirstName">
  <input type="text" placeholder="Last Name" @input="setLastName">
</template>

<script>
import { ref, computed } from 'vue'

export default {
  setup() {

    const user = ref({ firstName: '', lastName: '' })

    // functions to set first & last name
    const setFirstName = ($event) => {
      user.value.firstName = $event.target.value
    }

    const setLastName = ($event) => {
      user.value.lastName = $event.target.value
    }

    // computed
    const greeting = computed(() => {

      return 'Hello ' + user.value.firstName + ' ' + user.value.lastName
    })

    return {
      user,
      greeting,
      setFirstName,
      setLastName
    }
  }
}
</script>

note From the example above we also can see that event listening stays exactly the same as with the Options API. It’s only the way we implement computed properties that change.

Two-way databinding with v-model

Two-way databinding with v-model works the same as before, except now we use a ref instead of a data property.

Example: src/App.vue
<template>
  <p>Hello {{ user.firstName }} {{ user.lastName }}</p>
  <input type="text" placeholder="First Name" v-model="user.firstName">
  <input type="text" placeholder="Last Name" v-model="user.lastName">
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {

    const user = ref({ firstName: '', lastName: '' })

    return { user }
  }
}
</script>

Replacing the watch option with watch()

With the Options API we define watchers in an option called watch .

Example: watch option
watch: {
  watcherFunction(newValue, oldValue) {
    // watch a data property
    // use newValue/oldValue
  }
}

With the Composition API, we define watchers with the watch function. The function is imported from the ‘vue’ package and takes two arguments.

  • The data we want to watch.
  • A callback function that can accept the newValue and oldValue arguments.

Instead of defining a named function in the option, we define watch and do the watching in an anonymous or arrow function.

Example: watch
setup() {
  // data to watch
  const refName = ref(value)

  watch(refName, (newValue, oldValue) => {
    // use newValue and/or oldValue
  })
}

As an example, let’s take a message from an input in the template and watch for those changes with the watch function.

Example: src/App.vue
<template>
  <p>{{ message }}</p>
  <input type="text" placeholder="Message" v-model="message">
</template>

<script>
import { ref, watch } from 'vue'

export default {
  setup() {

    const message = ref('')

    watch(message, (newValue, oldValue) => {
      console.log('New Value', newValue)
      console.log('Old Value', oldValue)
    })

    return { message }
  }
}
</script>

Like the watch option, if we want to run the watcher on the initial value as well, we can specify immediate to be true as an optional third argument.

Example: immediate
watch(dataSource, (newValue, oldValue) => {
  console.log('New Value', newValue)
  console.log('Old Value', oldValue)
}, { immediate: true })

Multiple data sources

If we want to watch multiple data sources, we can specify them in an array. When we do, the newValue and oldValue also become arrays as newValues and oldValues .

Example: src/App.vue
<template>
  <p>Hello {{ firstName }} {{ lastName }}</p>
  <input type="text" placeholder="First Name" v-model="firstName">
  <input type="text" placeholder="Last Name"  v-model="lastName">
</template>

<script>
import { ref, watch } from 'vue'

export default {
  setup() {

    const firstName = ref('')
    const lastName  = ref('')

    watch(
      [firstName, lastName],
      (newValues, oldValues) => {
        console.log('New First Name', newValues[0])
        console.log('Old First Name', oldValues[0])
        console.log('New Last Name',  newValues[1])
        console.log('Old Last Name',  oldValues[1])
    })

    return { firstName, lastName }
  }
}
</script>

If our ref is an object instead of primitives, we have to use a getter callback function to accesss the value.

Example: src/App.vue
<template>
  <p>Hello {{ user.firstName }} {{ user.lastName }}</p>
  <input type="text" placeholder="First Name" v-model="user.firstName">
  <input type="text" placeholder="Last Name"  v-model="user.lastName">
</template>

<script>
import { ref, watch } from 'vue'

export default {
  setup() {

    const user = ref({ firstName: '', lastName: '' })

    watch(
      [
        // getter callbacks
        () => user.value.firstName,
        () => user.value.lastName
      ],
      (newValues, oldValues) => {
        console.log('New First Name', newValues[0])
        console.log('Old First Name', oldValues[0])
        console.log('New Last Name',  newValues[1])
        console.log('Old Last Name',  oldValues[1])
    })

    return { user }
  }
}
</script>

The watchEffect function

The watchEffect function works the same as watch , except for two things.

  • It runs immediately on the initial value, the same as when immediate:true in watch .
  • It automatically tracks dependencies in its body, we don’t explicitly add them as arguments.

It has to be imported from the ‘vue’ package and takes a callback function as its only argument. The data source’s value must be used in the callback function’s body in some way for Vue to be able to track it.

Syntax: watchEffect
setup() {
  // data to watch
  const refName = ref(value)

  watchEffect(() => {
    // use refName.value
  })
}

As an example, let’s take a message from an input in the template and log it to the console in the watchEffect function.

Example: src/App.vue
<template>
  <p>{{ message }}</p>
  <input type="text" placeholder="Message" v-model="message">
</template>

<script>
import { ref, watchEffect } from 'vue'

export default {
  setup() {

    const message = ref('Initial message')

    watchEffect(() => {
      // we must use data in the body in
      // some way for Vue to track it
      console.log(message.value)
    })

    return { message }
  }
}
</script>

Handling props

With the Options API we define props when we invoke a component in its parent’s template block.

Example: prop definition (parent)
<template>
  <greeting-message :firstName="firstName" :lastName="lastName" />
</template>

In the child component we capture the props in the props option of the config object. We can then use them in the child component either in the template with data binding, or in something like a computed property.

Example: props option (child)
export default {
  props: ['firstName', 'lastName']
}

When we use them in a computed property, we access the prop with the this keyword. But as mentioned earlier, we can’t use this in setup .

At the start of the lesson we mentioned the setup function has two optional parameters, one of which is a props object. Instead of this , we use the object to access props with dot notation.

Syntax: props object (parent)
setup(props) {

  const computedFunc = computed(() => {
    // access props through 'props' object
    return props.propName
  })

  return { computedFunc }
}

To demonstrate, we will do a full example with the root App component as our parent component and the following new component.

  • src/components/GreetingMessage.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   ├── GreetingMessage.vue
|   └── App.vue

The root App component takes a first and last name from inputs in the template and passes them to the GreetingMessage component.

Example: src/App.vue
<template>
  <greeting-message :firstName="user.firstName" :lastName="user.lastName" />

  <input type="text" placeholder="First Name" v-model="user.firstName">
  <input type="text" placeholder="Last Name" v-model="user.lastName">
</template>

<script>
import { ref } from 'vue'
import GreetingMessage from './components/GreetingMessage.vue'

export default {
  components: { GreetingMessage },
  setup() {
    const user = ref({ firstName: '', lastName: '' })

    return { user }
  }
}
</script>

The GreetingMessage component takes the two name props and combines them into a full name with a computed property.

Example: src/components/GreetingMessage.vue
<template>
  <p>Hello {{ fullName }}</p>
</template>

<script>
import { computed } from 'vue'

export default {
  setup(props) {

    const fullName = computed(() => {
      return props.firstName + ' ' + props.lastName
    })

    return { fullName }
  }
}
</script>

If we go to the browser and enter a first and last name in the input fields, the full name from the computed property will show.

Custom events and context

With the Options API we define an event in the emits option of a child component, then send it to a parent component with the $emit instance function.

Example: MainMenu.vue (child)
<template>
  <div>
    <p>Main Menu Component</p>
    <button @click="$emit('closeMenu')">Close Menu</button>
  </div>
</template>

<script>
  export default {
    emits: ['closeMenu']
  }
</script>

The parent listens to the event with event binding on the child component. We can then specify some sort of functionality to be executed when the event is captured, like changing a data property.

Example: src/App.vue (parent)
<template>
  <button @click="isMenuOpen = true">Open Menu</button>
  <main-menu v-show="isMenuOpen" @closeMenu="isMenuOpen = false" />
</template>

<script>
  import MainMenu from './components/MainMenu.vue'

  export default {
    components: {
      MainMenu
    },
    data() {
      return {
        isMenuOpen: false
      }
    }
  }
</script>

In the Composition API we use the second of the two optional parameters in the setup option, context . The parameter is a Javascript object that exposes 3 different properties, namely attrs , slots and the emit function.

emit works exactly the same as the special $emit instance function, except we can’t use it in the template block. We have to wrap it in a function (regular, anonymous or arrow) in setup .

Example: emit
setup(props, context) {

  function sendEvent() {
    context.emit('event')
  }

  return { sendEvent }
}

To demonstrate, let’s create a new component that will emit an event to the root App component.

  • src/components/MainMenu.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   ├── MainMenu.vue
|   └── App.vue

The MainMenu child component will emit an event when the user clicks on a button.

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <button @click="sendEvent">Close Menu</button>
  </div>
</template>

<script>
export default {
  setup(props, context) {

    function sendEvent() {
      context.emit('closeMenu')
    }

    return { sendEvent }
  }
}
</script>

note The props parameter doesn’t have a default value, so we have to include it even if we don’t use it.

In the root App component, we listen for the event and change a ref when it fires. The ref is used to determine if the component is shown or not.

Example: src/App.vue
<template>
  <button @click="isMenuOpen = true">Open Menu</button>
  <main-menu v-show="isMenuOpen" @closeMenu="isMenuOpen = false" />
</template>

<script>
import { ref }  from 'vue'
import MainMenu from './components/MainMenu.vue'

export default {
  components: {
    MainMenu
  },
  setup() {

    const isMenuOpen = ref(false)

    return { isMenuOpen }
  }
}
</script>

Replacing the provide & inject options with provide() and inject()

With the Options API, we specify data we want to provide to another component in the provide option.

Example: src/App.vue (providing component)
<template>
  <greeting-message />
</template>

<script>
  import GreetingMessage from './components/GreetingMessage.vue'

  export default {
    components: {
      GreetingMessage
    },
    provide: {
      username: 'John'
    }
  }
</script>

Once the data has been provided, we can inject it in the component where we want to use it with the inject option.

Example: src/component/GreetingMessage.vue (receiving component)
<template>
  <p>{{ username }}</p>
</template>

<script>
  export default {
    inject: ['username']
  }
</script>

In the Composition API we use the provide function to send data. This function must be imported from the ‘vue’ package and takes the key:value pair we want to provide as first and second arguments.

Syntax: provide()
provide(key, value)

To receive data, we use the inject function. This function must also be imported from the ‘vue’ package and takes the provided key as an argument.

Syntax: inject()
inject(key)

As an example, we’ll provide a username with a value of “John” from our root App component, which also imports and uses the GreetingMessage component.

Example: src/App.vue (providing component)
<template>
  <greeting-message />
</template>

<script>
import { provide } from 'vue'
import GreetingMessage from './components/GreetingMessage.vue'

export default {
  components: {
    GreetingMessage
  },
  setup() {
    // provide 'username'
    provide('username', 'John')
  }
}
</script>

Then, we’ll inject the username into a GreetingMessage component and store it in a constant. We’ll return the constant and output the value in the template.

Example: GreetingMessage.vue (receiving component)
<template>
  <p>{{ uName }}</p>
</template>

<script>
import { inject } from 'vue'

export default {
  setup() {

    // receive 'username'
    const uName = inject('username')

    return { uName }
  }
}
</script>

When we run the example in the browser, it will show the username in the GreetingMessage component.

Replacing lifecycle hooks

With the Options API we define lifecycle hooks as options in the config object.

Example: lifecycle hooks
export default {
  beforeCreate() { console.log('Parent beforeCreate()')},
  created()      { console.log('Parent created()')     },
  beforeMount()  { console.log('Parent beforeMount()') },
  mounted()      { console.log('Parent mounted()')     },
  beforeUpdate() { console.log('Parent beforeUpdate()')},
  updated()      { console.log('Parent updated()')     }
}

While this is still a perfectly valid approach, Vue gives us lifecycle functions that we can invoke inside the setup option.

We can translate a lifecycle hook to its function counterpart by prefixing it with on. The following table shows lifecycle hooks in the Options and Composition APIs.

Options APIHook inside setup()
beforeCreateNot used
createdNot used
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

note The beforeCreate and created lifecycle hooks are not used because the setup option already runs before the component is created. Any functionality we need at the component’s creation can be done inside setup .

Lifecycle functions aren’t available by default. We need to import the ones we want to use from the ‘vue’ package.

Example: lifecycle functions
import { onBeforeMount, onMounted } from 'vue'

export default {
  setup() {

    // creation
    console.log('setup()')

    // mounting
    onBeforeMount(() => { console.log('onBeforeMount()') })
        onMounted(() => { console.log('onMounted()')     })
  }
}

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.