Define a Component
There are multiple ways to define a component in Vue 3, we want to bring up some commonly seen patterns you may use in your everyday life.
1. Legacy Options API
The legacy Options API still works in Vue 3, in almost the same way! If you just copy & paste your Vue 2 components into a Vue 3 project, they'll probably work fine after some other minor issues are resolved.
That being said, it is strongly recommended to try out the new Composition API instead of sticking to the legacy Options API. Otherwise what is the point of upgrading from Vue 2 to Vue 3, right?
But don't get me wrong, Options API is still a great tool! It's still a valid way to define a component in Vue 3.
<template>
<div>
<div>Hello, {{ name }}, I'm {{ age }} years old.</div>
<div>I'll be {{ ageAfter3years }} years old after 3 years.</div>
<input v-model="name" />
<button @click="incrementAge">Increment age</button>
</div>
</template>
<script>
export default {
props: {
age: Number,
},
emits: ['incrementAge'],
data() {
return {
name: 'world',
}
},
computed: {
ageAfter3years() {
return this.age + 3
},
},
methods: {
incrementAge() {
this.$emit('incrementAge')
},
},
mounted() {
console.log('I am mounted!')
},
}
</script>
Make sure to check out the new
emits
option in Vue 3!There's also a new
defineComponent()
helper function in Vue 3. If you would like to see some type interfaces while defining components, you can use it like this:Example.vue<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: {
// Typical component stuff.
},
})
</script>
2. Composition API With <script setup>
Recommended
This is the most popular option at the moment. If you've leaned React Hooks API, you may find the coding styles very similar. If you don't, don't worry! It's actually very easy to understand.
By default there's no this
in <script setup>
whether you use function or arrow function. So if don't like this
, this will be good news!
<template>
<div>
<div>Hello, {{ name }}, I'm {{ age }} years old.</div>
<div>I'll be {{ ageAfter3years }} years old after 3 years.</div>
<input v-model="name" />
<button @click="incrementAge">Increment age</button>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, toRefs } from 'vue'
// Define props.
const props = defineProps<{
age: number
}>()
const { age } = toRefs(props)
// Define emitted events.
const emit = defineEmits<{
(e: 'incrementAge'): void
}>()
// Define data.
const name = ref('world')
// Define computed properties.
const ageAfter3years = computed(() => age.value + 3)
// Define methods.
const incrementAge = () => {
emit('incrementAge')
}
// Register life-cycle hooks.
onMounted(() => {
console.log('I am mounted!')
})
</script>
You may have noticed that we're using defineProps()
and defineEmits()
without importing them, this is because those functions are compiler macros. Vue will show a warning in console if you explicitly import and use them in a component.
3. Composition API With setup()
Another option is to use the new setup()
option in Options API:
<template>
<div>
<div>Hello, {{ name }}, I'm {{ age }} years old.</div>
<div>I'll be {{ ageAfter3years }} years old after 3 years.</div>
<input v-model="name" />
<button @click="incrementAge">Increment age</button>
</div>
</template>
<script>
import { computed, onMounted, ref } from 'vue'
export default {
props: {
age: Number,
},
emits: ['incrementAge'],
setup(props, context) {
// Define data.
const name = ref('world')
// Define computed properties.
const ageAfter3years = computed(() => props.age + 3)
// Define methods.
const incrementAge = () => {
context.emit('incrementAge')
}
// Register life-cycle hooks.
onMounted(() => {
console.log('I am mounted!')
})
// Return an object with all of the properties you want to expose to <template>.
return {
name,
ageAfter3years,
incrementAge,
}
},
}
</script>
It's almost the same with <script setup>
, the main differences are:
props
must be defined usingprops
option instead ofdefineProps()
, becausedefineProps()
only works in<script setup>
. This means we will not be able to use some convenient features like type-only props/emit declarations.- Emitted events must be defined using
emits
option instead ofdefineEmits()
, becausedefineEmits()
only works in<script setup>
. props
can be directly accessed in<template>
, but variables declared insetup()
must be returned in the function to make them accessible in<template>
.
If you're using SFC, we recommend you to just use <script setup>
instead because there's less boilerplate.
Since setup()
is a part of Options API, things like data()
, computed
, methods
, mounted()
could still work if they are defined.
For example, the following component will work without any warning/error:
<template>
<div>
<h1>Hello, {{ upperCaseName }}.</h1>
<button @click="sayMyAge">Say my age</button>
</div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
setup() {
const name = ref('world')
onMounted(() => {
console.log('[setup()] I am mounted!')
})
return {
name,
}
},
computed: {
upperCaseName() {
return this.name.toUpperCase()
},
},
methods: {
sayMyAge() {
console.log('I am 5 years old')
},
},
mounted() {
console.log('[mounted()] I am mounted!')
},
}
</script>
- There are 2
mounted
life-cycle hooks getting registered, and both of them works. - We could define data and methods in
setup()
, but that is now overlapping withdata
andmethods
options; this means we may have to check multiple places to find the source of a variable in a component.
Things would only get worse as your app gets bigger, so we strongly recommend you to avoid mixing Options API and Composition API like this at all cost!