Complete Guide Vue 3 — Composition API

Part 0 — Introduction Composition API
at DANA we are always improving our code quality & app performance. This is important for us to do. And the good news is that in Vue 3 there is a Composition API feature that aims to overcome the limitations and weaknesses of the Options API. We will learn more about Vue 3 Composition API, to make it easier for us to understand the Composition API, I have created a Todo App project and you can directly see my code on GitHub in this repo. You can also access the Todo app demo that has been created at this link. In Composition API we will learn Ref, Reactive, toRefs, Methods, Computed Getter & Setter, WatchEffect, Watch, Lifecycle, Component (Props & Emit).
Composition API in Vue 3 is optional, at the time this article was written programmers could still use the Options API in Vue 3 for Web App Development. The Composition API was created to overcome the limitations of Option API. Composition API feels useful in large and complex applications. This is because the concept of Composition API is to separate several logical concerns and even make the logic reusable. An example is :
setup () {
const { todos } = useListTodo()
const { addTodo} = useCrudTodo()
return {
todos,
addTodo
}
function useListTodo () {
const todos = ref('')
return {
todos
}
} function useCrudTodo () {
const addTodo = (e) => {
console.log(e)
}
return {
addTodo
}
}
}
The code above is an example for separating logical concerns, while for how to make a function reusable, suppose we want to create a function to store data into LocalStorage.
export default function saveDataToLocalStorage(listTodo) {
localStorage.setItem('todos', JSON.stringify(listTodo))
}
we save the code in the filesave-local-storage.js
, then we can use that function on all components, an example is as follows:
//import file save-local-storage.js
import saveToLocalStorage from '../components/save-local-storage.js'const addTodo = () => {
if(!todo.value) return
todos.list.unshift({
activity: todo.value,
isDone: false
})
todo.value = ''
saveToLocalStorage(todos.list)
}
Install Vite
Vite was created directly by the creator of Vue.js namely Evan You, Vite mission is to speed up the development process. If we use Vue CLI, the problem is we have to wait for all Applications in Vue to finish the bundle, so it will be quite a waste of development time, especially if the Application is quite complex and large. Unlike Vue CLI, Vite will only compile the files that are needed, namely the files that have changed. This will speed up the development process.
At the time this article was written, we had to install node in local with version >=12.0.0. And then we can install Vite with npm or yarn.
# npm 6.x
npm init vite@latest my-vue-app --template vue# npm 7+, extra double-dash is needed:
npm init vite@latest my-vue-app -- --template vue# yarn
yarn create vite my-vue-app --template vue# pnpm
pnpm create vite my-vue-app -- --template vue
Install & Config Tailwind CSS on Vite
If we use nom to install Tailwind CSS, then go to the project directory and run the following command in terminal
npm install -D tailwindcss postcss autoprefixernpx tailwindcss init -p
the above command will generate a filetailwind.config.js
and postcss.config.js
. then on filetailwind.config.js
edit and settings as follows:

create fileindex.css
in directorysrc
and add directive@tailwind
for each tailwind layer.
@tailwind base;
@tailwind components;
@tailwind utilities;
Last import file ./src/index.css
in file./src/main.js

we can run Vite for development and use Tailwind CSS with the commandnpm run dev
. As for production, we use the commandnpm run build
, The command will generate all the assets needed and store them indist
directory.
Part 1 — Ref, Reactive & toRefs
Ref, Reactive & toRefs is an important parts that we should know in Composition API. We will learn more about functions and examples of their use.
Setup
The Composition API uses the main function namedsetup
, function setup
contains all our logic.
export default {
setup(props, context) {
// Attributes (Non-reactive object, equivalent to $attrs)
console.log(context.attrs) // Slots (Non-reactive object, equivalent to $slots)
console.log(context.slots) // Emit events (Function, equivalent to $emit)
console.log(context.emit) // Expose public properties (Function)
console.log(context.expose)
}
}
function setup
has two arguments namelyprops
dan context
. props
inside of a setup
function are reactive and will be updated when new props are passed in. While context
the argument is a normal object and not reactive, By using ES6 we can destructuring context
as follows:
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
Keep in mind in Composition API we don't definedata
, methods
and computed
all will be replaced with concept function.
Ref
Ref is used to creating reactive and mutable variables. The ref can only be used for primitive data such as boolean, string and integer. Object Ref has one property .value
to set and get object Ref.
import { ref } from 'vue'export default {
setup(){
const todo = ref('todo is empty')
console.log(todo.value) // todo is empty
todo.value = 'I want to finish Vue course'
console.log(todo.value)//I want to finish Vue course
return {
todo
}
}
}
To access Ref in HTML template is:
<input v-model="todo" placeholder="Add anything..." type="text" name="todo"/>{{ todo }}
Reactive
Unlike Ref which can only be used for primitive data, Reactive can be used for objects. as an example:
import { reactive } from 'vue'export default {
setup(){
const todos = reactive({
activity: "I want to finish Vue Course",
isDone: false
}) setTimeout( () => {
todos.activity = "I want to finish Reactjs Course",
todos.isDone = false
}, 2000) return {
todos
}
}
}
if we use Ref, the data in the code above will not be reactive. Therefore we can use Reactive. Meanwhile to access Reactive in HTML template are:
{{ todos.activity }} - {{ todos.isDone }}
toRefs
toRefs is used to convert a Reactive object into a plain object. To better understand toRefs we give an example case study. Let’s say we want to access Reactive on an HTML template like this:
{{ activity }} - {{ isDone }}
Whereas in Composition API the code is like this:
import { reactive } from 'vue'export default {
setup(){
const todos = reactive({
activity: "I want to finish Vue Course",
isDone: false
}) setTimeout( () => {
todos.activity = "I want to finish Reactjs Course",
todos.isDone = false
}, 2000) return {
...todos
}
}
}
By only using javascript spread syntax without using toRefs, then we will lose its reactivity. So the data will not change, even though we will change the data after 2 seconds. To solve this case we use toRefs.
import { reactive, toRefs } from 'vue'export default {
setup(){
const todos = reactive({
activity: "I want to finish Vue Course",
isDone: false
}) setTimeout( () => {
todos.activity = "I want to finish Reactjs Course",
todos.isDone = false
}, 2000) return {
...toRefs(todos)
}
}
}
toRef
Unlike toRefs which is used for Reactive, toRef is used to convert a single reactive object property to Ref. Here’s the difference
toRef
const state = reactive({
foo: 1,
bar: 2
})const fooRef = toRef(state, 'foo')
/*
fooRef: Ref<number>,
*/
toRefs
const state = reactive({
foo: 1,
bar: 2
})const stateAsRefs = toRefs(state)
/*
{
foo: Ref<number>,
bar: Ref<number>
}
*/
Part 2 — Methods, Computed Getter & Setter
Methods
Unlike Option API, we have to declare function inside methods, in Composition API we don’t need to, see the difference.
data() {
return {
count: 4
}
},
methods: {
increment() {
// `this` will refer to the component instance
this.count++
}
}
By using Composition API will be shorter.
setup(){
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
Computed Getter & Setter
To use Computed we have to call computed in Vue. For exampleimport { computed } from ‘vue’
For an example of using a Computed getter & setter, see the code below.
import { ref, computed } from 'vue'export default {
setup(){
const count = ref(1)
const result = computed({
get: () => count.value + 10,
set: val => {
console.log('val',val) //0
count.value = val - 5
}
})
result.value = 0
console.log("count", count.value) //-5
console.log("result", result.value) //5
return {
count,
result
}
}
}
To call Computed in HTML template, it’s easy:
<template>
{{ count }} = {{ result }}
</template>
But if you don’t want to use computed getter & setter, here’s an example:
import { ref, computed } from 'vue'export default {
setup(){
const count = ref(1)
const result = computed(() => {
return count.value+10
})
result.value = 0
console.log("count", count.value) // 1
console.log("result", result.value) // 11
return {
count,
result
}
}
}
Part 3 — Watch & WatchEffect
watch
watch
is useful for monitoring changes in state, we can do something to the state if there is a change. inwatch
we can also get old value or previous value.
To use watch
& watchEffect
we import first:
import { watchEffect, watch } from 'vue'
example of using watch
watch(todo, (newValue, prevValue) => {
console.log("todo", todo.value)
console.log("prev", prevValue)
})
To monitor state changes in more than one, the example is:
watch([todo, todos], (newValue, prevValue) => {
console.log("todo", todo.value)
console.log("todos", todos)
console.log("new value", newValue)
console.log("prev value", prevValue)
})
watchEffect
watchEffect
has the same function namely monitoring state changes. The difference is watchEffect
is suitable for monitoring more than one state. which should be noted watchEffect
can not access old values or previous values in the state.
watchEffect( () => {
console.log("todo", todo.value)
console.log("todo list", todos.list)
})
especially for debugging, we can use optionsonTrigger
and onTrack
but keep in mind this only applies to development, if in production onTrigger
and onTrack
will not run.
watchEffect(
() => {
console.log("todo", todo.value)
},
{
onTrigger(e) {
console.log("onTrigger", e)
},
onTrack(e) {
console.log("onTrack", e)
}
}
)
Besides that, there are also optionsflush
, optionsflush
the default value ispre
which means it will be executed before the component is rendered we can change the value to post
which means it will be executed after the component is rendered.
watchEffect(
() => {
console.log("todo", todo.value)
},
{
flush: 'post'
}
)
Part 4 — Lifecycle Hooks
Lifecycle in Composition API is not too different from Options API, there are several lifecycles that we need to understand:
onBeforeMount
- called before mounting beginsonMounted
- called when the component is mountedonBeforeUpdate
- called when reactive data changes and before re-renderonUpdated
- called when reactive data changes and after re-renderonBeforeUnmount
- called before the Vue instance is destroyedonUnmounted
- called after the instance is destroyedonActivated
- called when a keep-alive component is activatedonDeactivated
- called when a keep-alive component is deactivatedonErrorCaptured
- called when an error is captured from a child component
maybe some of us will ask, in Options API there are created
and beforeCreated
lifecycle is Composition API removed created
and beforeCreated
lifecycle? The answer is that created
and beforeCreated
lifecycles are replaced with setup()
here’s an example of how to use them.
//Options API
export default {
data() {
return {
val: 'hello world'
}
},
created() {
console.log('Value of val is: ' + this.val)
}
}
if you use Composition API it will look like this:
import { ref } from 'vue'
export default {
setup() {
const val = ref('hello world')
console.log('Value of val is: ' + val.value)
return {
val
}
}
}
Keep in mind that if you want to use onMounted
onUpdated
other hooks, don’t forget to import them first.
import { onMounted, onUpdated } from 'vue';
In addition, for debugging we can use onRenderTriggered
and onRenderTriggered
hooks, for more details we can read the documentation here.
Part 5 — Component (Props & Emit)
props
and emit
are used for passing data between components. props
are used to send data from the parent component to the child component. Meanwhile emit
can be used as a trigger event for passing data from the child component to the parent component.
//called from parent component
<list-todo
:todos="todos.list"
@delete-todo="deleteTodo"
/>
we can use access props and use emit in child component as follows:
import { onMounted } from "vue"
export default {
props: {
todos: {
type: Array,
default: [],
}
},
setup(props, { attrs, slots, emit }) {
const handleDeleteTodo = (index) => {
emit('delete-todo', index)
}
onMounted(() => {
console.log("data from parent : ", props.todos)
})
return {
handleDeleteTodo,
}
}
}
need to remember that setup()
has two arguments, namely props
and context
. context
can be destructured to { attrs, slots, emit, expose }
we can emit using context
emit('eventName', [argumen])
maybe that’s what I can write, you can read the full code on my GitHub at this link. Hope it is useful. If you have any questions or suggestions, please contact me at https://dikiharifwibowo.github.io/.