Vue 3 Tutorial: Advanced component options

What about the advanced options? You already know Vue 2, and creating some refs, computed or methods it’s nothing special, right? What should you do if you want to use something more complex, which of course is needed in your application. There are few differences between Vue 2 and Vue 3 which you need to know. Like in the first article with basic component options, here in advanced section you also have more control on what and how you are going to use specific methods than in Vue 2. Let’s dig into more.

​​In this article you will find answers to following questions:

  • How to use Vue.js composition API advanced options?
  • How to use slots in Vue 3?
  • How to use router and route in Vue 3?
  • How to use emit, and declare props in Vue 3?
  • What is expose in Vue 3 and how to use it?
  • How to combine Vue 2 options API and Vue 3 composition API in one application?

Emit

Main difference between Vue 2 options API emit and Vue 3 emit is that in script tag you have to specify what kind of emit you are going to use with defineEmits, which is available out of the box, so you don’t need to import this method from vue:

<script setup>
const $emit = defineEmits(['add', 'delete'])
 
// use in script tag
$emit('add')
</script>

Again, in template, $emit is available out of the box.

Vue 3 Props

Props are similar to emit, you also have to define them by using defineProps. Other things look very similar to Vue 2 options API:

<script setup>
const props = defineProps({
 name: {
   type: String,
   required: true,
 },
})
</script>

If you are going to use props directly inside the script setup tag, you have to save the value of it to some variable. After that, name props will be available in props.name.

Router and Route

What if you want to use router or route inside script setup tag? Remember how you were able to use this.$router in Vue 2, and use context.root.$router with composition API Vue 2 package? Now, you have to manually import it from vue-router package:

<template>
 <BaseComponent />
</template>
 
<script setup>
import BaseComponent from '@/components/BaseComponent.vue'
import { useRouter, useRoute } from 'vue-router'
 
const $router = useRouter()
const $route = useRoute()
</script>

Now you have access to all options on router and route. You can name it whatever you want, but look at that $ sign. If you keep naming consistent as in Vue 2, it’s going to be easier to maintain the code in case there’s someone new in the team transferring from Vue 2 to Vue 3. It’s even better for you, because you won’t get lost in new code. Remember, you don’t have to use .value here. All the settings are available directly on those two variables.

Slots

When it comes to slots, the way of using them in script setup is quite similar to how you handle other built-in methods. You have to access them directly from useSlots composable, available in vue package:

<template>
 <BaseComponent />
 {{ $slots }}
</template>
 
<script setup>
import { useSlots } from 'vue'
import BaseComponent from '@/components/BaseComponent.vue'
 
const $slots = useSlots()
</script>

Again, you can name it whatever you want, but to keep consistent, let it be $slots. In template, $slots are available directly.

Template Refs

When you want to bind reference to component or HTML element, you have to use normal ref binding, like when you are creating any other reactive variable:

<template>
 <BaseComponent ref="baseComponent" />
</template>
 
<script setup>
import { ref } from 'vue'
import BaseComponent from '@/components/BaseComponent.vue'
 
const baseComponent = ref(null)
</script>

Yes, you don’t need to define the ref value inside the script tag, Vue 3 is handling it behind the scenes. When you create a ref on a component or HTML element, and add the same ref name inside the script tag, Vue 3 is handling writing that reference into the created variable. Easy and cool!

If you are using template reference inside v-for loop, everything works the same, but you are receiving an Array of ref elements (while writing this article, that feature is still bugged*, but there is a workaround!).

*Update 20.07.2022: With newest Vue 3 version (tested on 3.2.37), the template ref inside v-for loop works as expected. You can get rid of any workarounds, and use it directly, like in documentation. It's still handy that you can use your own function to implement complex operations on refs but you don't have to use it to hack that particular functionality:

<template>
    <div>
        <div ref="elements" v-for="n in 5" :key="n">
            I'm one of many refs!
        </div>
    </div>
</template>

<script setup>
import { onMounted, ref } from "vue"

const elements = ref(null)

onMounted(() => {
    console.log(elements.value)
})
</script>

Now in console you will see:

Expose

Vue 3 components with script setup are closed, so you can’t access any data when communicating between them. But you can change that behavior by using the built-in method defineExpose. That’s quite convenient, because with that method, other developers will know what functionality is available outside components, it’s like private and public methods:

<template>
 <div>{{ name }}</div>
</template>
 
<script setup>
import { ref, computed } from 'vue'
 
const name = ref('John')
const age = ref(26)
 
const combinedText = computed(() => `${name.value} is ${age.value} years old`)
 
defineExpose({
 name,
 combinedText,
})
</script>

Now in parent component:

<template>
 <BaseComponent ref="baseComponent" />
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
import BaseComponent from '@/components/BaseComponent.vue'
 
const baseComponent = ref(null)
 
onMounted(() => {
 console.log(baseComponent.value.name)
 console.log(baseComponent.value.age)
 console.log(baseComponent.value.combinedText)
})
</script>

And your console.logs should print:

As you can see, age property is acting like a private field, and we can’t access it from the parent component. 

Vue 2 options API and Vue 3 composition API – how to combine them?

Imagine you already have a big, enterprise level project, and you want to migrate to Vue 3, but even thinking about it gives you a headache.

Don’t worry, you can combine both Vue 2 options API and Vue 3 composition API, and make that migration painless:

<template>
 <div>{{ name }} {{ surname }}</div>
</template>
 
<script setup>
import { ref } from 'vue'
 
const name = ref('John')
</script>
 
<script>
export default {
 data() {
   return {
     surname: 'Smith',
   }
 },
}
</script>

Now you will see John Smith in your web browser, because Vue 3 has backwards compatibility, which allows you to combine both APIs, or even use options API in one component, and Vue 3 composition API in another. That is really helpful when migrating your project to a new standard. 

🔔 Remember, you can’t use name ref inside the options API. So keep in mind that when migrating, you have to migrate whole singular logic to keep everything working fine:

<template>
 <div>{{ name }}</div>
 <div>{{ surname }}</div>
 <div>
   {{ combinedName }}
 </div>
</template>
 
<script setup>
import { ref } from 'vue'
 
const name = ref('John')
</script>
 
<script>
export default {
 data() {
   return {
     surname: 'Smith',
   }
 },
 computed: {
   combinedName() {
     return `${this.surname} ${name}` // name is not defined!
   },
 },
}
</script>

🔔 🔔 Also, remember that Vue 3 composition API has an advantage over Vue 2 options API – if you create two variables with the same name, but different value, the composition API is taking the lead:

Vue 3 Lifecycle hooks

There’s also a quite big change in terms of lifecycle hooks. Basics stay the same, but there is no more created available in the Vue 3 component. Now created is everything that you are going to execute inside the script setup tag. So basically every function directly fired is a function which is going to be fired like in Vue 2 created. To be more specific, everything fired inside script setup tag is fired just before the Vue 2 beforeCreate method.

So if you want to get some data from API, you can use IIFE – Immediately Invoked Function Expression combined with async/await:

<script setup>
import { ref } from 'vue'
const loading = ref(true)
const url = `https://rickandmortyapi.com/api/character?page=3`;
(async () => {
 try {
   const response = await fetch(url);
   data.value = await response.json();
 } catch (e) {
   console.warn(e);
 } finally {
   loading.value = false
 }
})()
</script>

Of course it’s up to you, if you are going to define a function and then fire it, but for now IIFE is the best option when it comes to asynchronous calls. 

There’s already implemented an experimental feature which is Top Level Await. In this case, you can use await directly inside the script setup tag and combine it with template component <Suspense> which is going to keep the loading state of the Vue 3 component. Unfortunately, it’s in the experimental stage and everything could change in terms of using it. If you want to read more about it, click here.

Other lifecycle methods are transferred to hooks, so you specifically define which one of them you are going to use:

<template>
 <div ref="container">Am I here already?</div>
</template>
 
<script setup>
import { onMounted, ref } from 'vue'
 
const container = ref(null)
 
console.log(container.value)
onMounted(() => {
 console.log(container.value)
})
</script>

Like in this example, and like in Vue 2, template ref is available after component rendering in the onMounted hook. Detailed use of all lifecycle hooks is explained in docs.

Summary

I hope you will find this article interesting and useful, and starting your adventure with Vue.js will be flawless and great. Try it asap, it’s worth it, and stay tuned for another Vue 3 article coming soon. For more information on Vue 3 composition API, check out ❗How to start with Vue 3 composition API and why.

Remember to share this article and sign up to our newsletter! ;)

Great ideas require great technology solutions