Vue3 Composition API Deep Dive

📢 This article was translated by gemini-2.5-flash

Project Setup

Requires Node.js 16.0 or higher. Run this command:

1
npm init vue@latest

This command will install and execute create-vue.

setup

Execution Timing

It runs even before beforeCreate(), so this isn’t available.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<script>
export default{
    setup(){
        console.log('setup', this)
    },
    beforeCreate(){
        console.log('beforeCreate')
    }
}
</script>

After execution, you’ll see setup undefined printed first, then beforeCreate.

Data Access

To use data or functions defined in setup() within your <template>, you must return them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script>
export default{
    setup(){
        // data
        const msg = 'hello vue3'
        // function
        const logMsg = () => {
            console.log(msg)
        }
        // return
        return{
            msg,
            logMsg
        }
    }
}
</script>

Only after returning can you use them in the <template>.

1
2
3
4
<template>
<div>{{ msg }}</div>
<button @click="logMsg">button</button>
</template>

Syntactic Sugar

Returning everything manually is a bit of a pain. Luckily, you can just add setup to your <script> tag, and it handles the return automatically. So, the above example becomes:

1
2
3
4
5
6
<script setup>
const msg = 'hello vue3'
const logMsg = () => {
        console.log(msg)
    }
</script>

Of course, it still returns under the hood; you just don’t have to write it out.

Reactive Data

reactive

The reactive() function takes an object and returns a reactive object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<script setup>
import { reactive } from 'vue'
const state = reactive({
  count: 100
})
const addCount = () => {
  state.count++
}
</script>

<template>
<div>
  <div>{{ state.count }}</div>
  <button @click="addCount">+1</button>
</div>
</template>

ref

ref() takes either a primitive or complex type and returns a reactive object. Under the hood, it wraps the original data in an object, making it a more complex type. Essentially, it uses reactive() internally for reactivity.

Because of this, you need to access the data via .value in <script> tags. But in <template>, you can use the variable directly. Here’s the previous example rewritten with ref():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script setup>
import { ref } from 'vue'
const c = ref(0)
const addC = () => {
  c.value++
}
</script>

<template>
<div>
  <div>{{ c }}</div>
  <button @click="addC">+1</button>
</div>
</template>

In real-world development, using only ref() often leads to more flexible and consistent code.

computed

Computed properties, using computed(), are similar to Vue 2, but now they’re just functions you can call anywhere.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script setup>
import { computed, ref } from 'vue'

const list = ref([1, 2, 3, 4, 5, 6, 7, 8])

const computedList = computed(() => {
    return list.value.filter(item => item > 2)
})
</script>

<template>
<div>{{ list }}</div>
<div>{{ computedList }}</div>
</template>

Properties created this way are read-only. If you need a writable computed property, you’ll have to explicitly declare get() and set(). Here’s an example from the official docs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script>
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0
</script>

Source: https://cn.vuejs.org/api/reactivity-core.html#computed

watch

The watch() function also tracks changes in one or more data sources, executing a callback when data changes. However, it now includes two additional parameters: immediate and deep.

  • Single Data Source
1
2
3
watch(count, (newValue, oldValue) => {
    console.log('count changed', oldValue, newValue)
})
  • Multiple Data Sources
1
2
3
watch([count, name], ([newCount, newName], [oldCount. oldName]) => {
    console.log('count or name has changed', [newCount, newName], [oldCount. oldName])
})

Here’s an example. I’ve used simplified Pug here, but it should be easy enough to understand.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
const name = ref('John')

const addCount = () => count.value++
const changeName = () => name.value = 'Mike'

// Watching a single data source
watch(count, (newValue, oldValue) =>{
    console.log('count changed', oldValue, newValue)
})

// Watching multiple data sources
watch([count, name], (newArr, oldArr) => {
    console.log('count or name changed', oldArr, newArr)
})
</script>

<template lang="pug">
    div count: {{ count }}
    button(@click="addCount") +1
    div name: {{ name }}
    button(@click="changeName") change name
</template>

immediate

immediate means it runs immediately. So, when the page loads, the watcher will trigger once. At this point, oldValue will be undefined.

1
2
3
4
5
watch(count, (newValue, oldValue) =>{
    console.log('count changed', oldValue, newValue)
}, {
    immediate: true
})

deep

deep enables deep watching. By default, watch performs a shallow watch, which won’t detect changes within complex types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref, watch } from 'vue'

const userInfo = ref({
    name: 'John',
    age: 18
})

const changeUserInfo = () => {
    userInfo.value.age++
}

watch(userInfo, (newValue) => {
    console.log('userInfo changed', newValue)
}, {
    deep: true
})
</script>

<template lang="pug">
    div {{ userInfo }}
    button(@click="changeUserInfo") change userInfo
</template>

Watching a Single Property of a Complex Type

Using deep will watch all properties of a complex type. This means the function will execute whenever any property changes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
import { ref, watch } from 'vue'

const userInfo = ref({
    name: 'John',
    age: 18
})

const changeAge = () => {
    userInfo.value.age++
}
const changeName = () => {
    userInfo.value.name = 'Mike'
}

watch(() => userInfo.value.age, (newValue, oldValue) => {
    console.log('age changed', oldValue, newValue)
})
</script>

<template lang="pug">
    div {{ userInfo }}
    button(@click="changeAge") change age
    button(@click="changeName") change name
</template>

This way, it only triggers when age changes, and the return value is the age itself.

Lifecycle Hooks

Vue3 Lifecycle Comparison

Option APIComposition API
beforeCreate/createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted

The main difference with the Composition API is that these are now function calls, and you can call them multiple times.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script setup>
import { onMounted } from 'vue';

// For Composition API, `beforeCreate` and `created` logic goes directly in `setup`.
const getList = () => {
    console.log('Fetching data from the backend')
}

// Executes immediately when the page loads, making the request.
getList()
    
// Lifecycle hooks can be called multiple times; they'll execute in order.
onMounted(() => {
    console.log('Logic one')
})
onMounted(() => {
    console.log('Logic two')
})
</script>
This post is licensed under CC BY-NC-SA 4.0 by the author.