Vue 3 enables us to create components using script setup, reducing the amount of code to write. Here’s how to capitalize on it!
With Vue 3 not only comes the ability to use the composition API but also the capacity to create components using script setup to minimize the amount of code we have to write. Script setup in itself is mostly syntactical sugar, and for that reason we have a few “magic” functions (or compiler macros if you want to be technical) that you should familiarize yourself with if you are going to be leveraging its capabilities.
When creating a component with script setup, one of the first things you will probably run into is the need to create and define component properties.
In the options API, this is extremely clear as we define the props property in the object that is exported at the top level.
<script>
export default {
props: {
myProp: { type: String, required: true },
optional: { type: Boolean, default: false }
}
}
</script>
To be able to achieve this in script setup, we use a magic function called defineProps. The function receives either an array of strings to define property names, or the same object syntax that you are already used to with the options API.
The above example, written in script setup and leveraging defineProps is written as follows:
<script setup>
defineProps({
myProp: { type: String, required: true },
optional: { type: Boolean, default: false }
})
</script>
Note that if you want to use these props directly in your script, for example to create a computed property, you can capture the return value of the function—a reactive object.
<script setup>
const props = defineProps({
myProp: { type: String, required: true },
optional: { type: Boolean, default: false }
})
const componentTitle = computed(() => props.optional ? 'Optional' : 'Required' )
</script>
For a simplified version, we can write this using array syntax. However, note that we lose the ability to warn our developers of type errors.
<script setup>
defineProps(['myProp', 'optional'])
</script>
defineEmits
A component will usually interact with other components through its public API which consists of props and emits. We just learned above how to declare props in our script setup components, but how do we go about declaring which events our component emits?
Just as we have defineProps, Vue 3 also grants us the defineEmits function. This function takes one parameter, an array of strings or an object—just as defineProps did.
Consider the following component that emits a click event.
<script setup>
const emit = defineEmits(['click'])
</script>
<template>
<button @click="emit('click')" />
</template>
The component uses the defineEmits function to declare that this component will emit a click event. This helps reduce errors, as Vue will warn you if you emit an event that is not declared in the defineEmits declaration, and it allows us to also declare emit types—more on this in a second.
The defineEmits function returns another function which we name emit, and we use this function to emit the actual click event on our button input. This function can also be used internally within the script section to dynamically emit events when necessary.
If we wanted to be a bit safer, we can use object syntax (just like with our props) to declare a type of emit, and even a validator.
<script setup>
const props = defineProps({
number: { type: Number, required: true }
})
const emit = defineEmits({
click: {
type: Number,
validator: num => num > 0
}
})
const emitValue = () => {
emit(props.number + 1)
}
</script>
<template>
<button @click="emitValue" />
</template>
There’s a couple of changes here. We’ve added a prop number to enhance our example. This allows us to add an emit value to our click event so that we can demonstrate the object syntax of defineEmits.
Notice that our click emit now declares a type of Number and even a validator that checks that the emitted value is always greater than 0. This is quite valuable and allows to catch errors in development early, as these warnings will be reported in browser console and test suites like Vitest.
Notice additionally that the emit function is now being used within the wrapper function emitValue. As described above, the function returned by defineEmits can be used to emit a value within the script block itself if needed.
In Vue 3 the v-model defaults considerably changed from the way that they were declared in Vue 2. We now use modelValue as the default property for a model and update:modelValue for the default emit value of a v-model in a component. I will not go into depth on the new v-model syntax as this is out of the scope of the article. Instead, we will explore how we can leverage defineModel to quickly create several v-model-able properties within our component.
Consider the following example:
<script setup>
defineProps({
modelValue: String,
name: { type: String, default: 'Marina' }
})
defineEmits(['update:modelValue', 'update:name'])
</script>
defineModel allows us to quickly declare a “model” for both of the props above without having to declare them additionally in our emits.
<script setup>
const modelValue = defineModel({ type: String })
const name = defineModel('name', { type: String, default: 'Marina' })
</script>
Notice that the first defineModel call does not pass a string initially to declare the name of the model as modelValue. This is because modelValue is already the default, can we can safely skip it.
The second example creates the name property for us, and under the hood also declares the update:name emit so that the consuming component can bind to it using v-model:name syntax.
Components that are created using script setup syntax are closed by default. This means that unlike regular components, the properties that we create on the component will not be available to accessed using template refs or VM accessor in tests.
In order to make certain properties of our component available to be externally accessed in this manner, we must declare them as exposed with defineExpose as follows.
<script setup>
const privateProp = ref(123)
const publicProp = ref(234)
defineExpose({
publicProp
})
</script>
If someone where to try to access our component’s publicProp through a $ref or through the .VM in a test, then component.publicProp would equal the reactive value of 234.
However, if someone tried to access the privateProp property, they would get undefined as that property is not publicly available.
Take note that exposing properties is to be used with extreme care and normally would be considered a code smell unless explicitly documented. Always err on the side of exposing a public API through an emit instead when possible.
In the options API, a component can be created declaring custom component options. These options are normally declare to provide a static property that is not reactive but useful to the internals of the component.
In order to declare options in a script setup component, we must declare it using the defineOptions function.
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
myOption: 123
}
})
</script>
Notice that before creating our custom myOption option I’ve chosen to add an inheritAttrs call. I’ve done this on purpose because most of the time when you find yourself in need to declare options in a script setup component you will do it in order to control attribute fallthrough in your component.
I hope this post has helped you understand these compiler macros for use in your Vue applications!
Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.