- read

The Anatomy of a Vue 3 Component

Fotis Adamakis 47

VVue 3 has made significant progress and reached a strong level of maturity with Composition API & Script Setup being the recommended and widely adopted syntax for writing our components. This differs from the options API, which was the preferred approach in Vue 2.

Let's explore together all the ingredients of a Vue 3 component using Composition API and Script Setup.

tl;dr 👇

<script setup>
import {
ref,
reactive,
defineAsyncComponent,
computed,
watch,
onMounted,
} from "vue";

import useComposable from "./composables/useComposable.js";
import TheComponent from "./components/TheComponent.vue";

const AsyncComponent = defineAsyncComponent(() =>
import("./components/AsyncComponent.vue")
);

console.log("Equivalent to created hook");
onMounted(() => {
console.log("Mounted hook called");
});

const enabled = ref(true);
const data = reactive({ variable: false });

const props = defineProps({
elements: Array,
counter: {
type: Number,
default: 0,
},
});

const { composableData, composableMethod } = useComposable();

const isEmpty = computed(() => {
return props.counter === 0;
});

watch(props.counter, () => {
console.log("Counter value changed");
});

const emit = defineEmits(["event-name"]);
function emitEvent() {
emit("event-name");
}

function getParam(param) {
return param;
}

</script>

<template>
<div class="wrapper">
<TheComponent />
<AsyncComponent v-if="data.variable" />
<div
class="static-class-name"
:class="{ 'dynamic-class-name': data.variable }"
>
Dynamic attributes example
</div>
<button @click="emitEvent">Emit event</button>
</div>
</template>

<style scoped>
.wrapper {
font-size: 20px;
}
</style>

Script Setup

A Single File Component still consists of 3 parts. The template, the styles and the script. The two former are almost identical between vue 2 and vue 3, with the latter being completely revamped to the so-called script setup syntax. Declararion is simply adding the setup keyword in the script tag.

<script setup>
// Component logic goes here
// --
// Every variable and method
// will be automatically available
// in the template
</script>

By doing so a lot of boilerplate can be removed because every variable and method declared inside the script setup, will be automatically available in the template.

<script setup>
const text = "Hello world!"

function getParam(param) {
return param;
}

</script>

<template>
{{ text }}
{{ getParam(1) }}
</template>

Reactive Data Declaration

A variable declared with the keyword const, let or var is not automatically reactive. To make it reactive we need to declare it using one of the following helpers.

  • reactive for complex types (Arrays, Objects, Maps, Sets)
  • ref for primitives (String, Number, Boolean)
import { ref, reactive } from 'vue'

const enabled = ref(true)
const data = reactive({ variable: false })

More info about why both ref and reactive are needed

Component Declaration

Simply importing a component will make it available in the template. In the case of a lazy-loaded component, the defineAsyncComponent should be used.

import { defineAsyncComponent } from "vue";
import TheComponent from "./components/TheComponent.vue";
const AsyncComponent = defineAsyncComponent(() =>
import("./components/AsyncComponent.vue")
);

Computed

Computed values work the same but the syntax is quite different. Declaration is done using the computed helper that accepts a callback and returns the reactive variable.

import { computed } from "vue";

const count = 0;

const isNegative = computed(() => {
return count < 0;
});

Watcher

A watcher can be declared in a similar manner, accepting as a parameter a source and a callback. The source can be one of the following:

  • A getter function or a computed that returns a value
  • A ref
  • A reactive object
  • An array of any of the above
import { watch, ref } from "vue";
const counter = ref(0);
watch(counter, () => {
console.log("Counter value changed");
});

WatchEffect

WatchEffect, behaves like a watch but only expects a callback. The sources that will trigger the effect are automatically detected.

import { reactive, watchEffect } from "vue"

const state = reactive({
count: 0,
name: 'Leo'
})

watchEffect(() => {
// Runs immediately
// Logs "Count: 0, Name: Leo"
console.log(`Count: ${state.count}, Name: ${state.name}`)
})

state.count++ // logs "Count: 1, Name: Leo"
state.name = 'Cristiano' // logs "Count: 1, Name: Cristiano"

More info about watch Vs watchEffect

Lifecycle Hooks

A component has multiple lifecycle hooks that we can utilise according to our needs:

[onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onErrorCaptured, onRenderTracked, onRenderTriggered, onActivated, onDeactivated]

The usage is as follows:

import { onMounted } from "vue";
console.log("Equivalent to created hook");
onMounted(() => {
console.log("Mounted hook called");
});

More info about Lifecycle hooks

Define Props

Props declaration is done with the defineProps script macro. Script macros don’t need to be imported and the variableselements and counter in the following example will be automatically available in the template. All the validation options are supported.

defineProps({
elements: Array,
counter: {
type: Number,
default: 0,
},
});

Define Emits

Emits are declared with another script macro. First, we need to declare them with the defineEmits helper and then use the return value as the emitter.

<script setup>
const emit = defineEmits(["event-name"]);
function emitEvent() {
emit("event-name");
}
</script>

<template>
<button @click="emitEvent">Emit event</button>
</template>

More info about script setup macros

Composables

Composables are simple statefull functions that can be used to share data and logic between components. They replace mixins with a declarative and more easy to understand and test syntax.

A very basic example of a composable is the following:

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// by convention, composable function names start with "use"
export function useMouse() {
// state encapsulated and managed by the composable
const x = ref(0)
const y = ref(0)

// a composable can update its managed state over time.
function update(event) {
x.value = event.pageX
y.value = event.pageY
}

// a composable can also hook into its owner component's
// lifecycle to setup and teardown side effects.
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

// expose managed state as return value
return { x, y }
}
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

Mixins vs Composables

Additional Resources

Vue 3 - Composition API

15 stories