Vue has two ways to write a component. This track uses the Composition API for all 28 lessons; this one covers the other, because you will have to read it.
The Options API is not deprecated, is not going away, and is not wrong. It is what every Vue 2 codebase uses, what a great deal of Vue 3 code still uses, and what most tutorials older than about 2022 will show you.
The same component, both ways
Here is the demo application's status badge as it is actually written:
<script setup>
import { computed } from "vue";
import { STATUS_META } from "../../utils/format";
const props = defineProps({ status: { type: String, required: true } });
const meta = computed(() => STATUS_META[props.status] ?? { label: props.status, variant: "secondary", icon: "bi-question" });
</script>
<template>
<span class="badge d-inline-flex align-items-center gap-1" :class="`text-bg-${meta.variant}`">
<i class="bi" :class="meta.icon"></i>{{ meta.label }}
</span>
</template>and the same component in the Options API:
<script>
import { STATUS_META } from "../../utils/format";
export default {
name: "StatusBadge",
props: {
status: { type: String, required: true },
},
computed: {
meta() {
// `this` is the component instance. `this.status` reaches the prop.
return STATUS_META[this.status] ?? { label: this.status, variant: "secondary", icon: "bi-question" };
},
},
};
</script>
<template>
<span class="badge d-inline-flex align-items-center gap-1" :class="`text-bg-${meta.variant}`">
<i class="bi" :class="meta.icon"></i>{{ meta.label }}
</span>
</template>The template is identical. That is the thing to notice first — the two APIs differ
only in how the script half is organised. Every directive, every binding, slots, events and
v-model all work exactly the same.
The options
A component is an object whose keys Vue understands:
export default {
name: "ReelPlayer",
// Declared the same way as defineProps.
props: {
reel: { type: Object, required: true },
active: { type: Boolean, default: false },
},
// Declared the same way as defineEmits.
emits: ["like", "view", "share"],
// MUST be a function returning an object. Everything it returns is reactive.
data() {
return { progress: 0, isPlaying: false };
},
// Each becomes a cached, derived property. `computed:` == computed().
computed: {
hasVideo() {
return Boolean(this.reel.video?.url);
},
},
// `watch:` == watch(). The key is what to watch.
watch: {
active(isActive, wasActive) {
if (isActive) this.play();
},
// The options form of { deep: true, immediate: true }
reel: { handler: "reload", deep: true, immediate: true },
},
// Lifecycle hooks are top-level keys, without the `on` prefix.
mounted() {},
beforeUnmount() {},
// Plain functions. Called from the template by name.
methods: {
play() {
this.$refs.videoEl.play();
this.$emit("view", this.reel.id);
},
},
};The mapping is almost one to one:
Options API Composition API
------------------- -------------------------
data() ref() / reactive()
computed: { x() {} } const x = computed(() => ...)
watch: { y() {} } watch(y, () => ...)
methods: { f() {} } function f() {}
mounted() onMounted(() => ...)
beforeUnmount() onBeforeUnmount(() => ...)
props defineProps()
emits defineEmits()
this.$refs.el const el = ref(null)
this.$emit(...) const emit = defineEmits(); emit(...)this, and how it breaks
Everything is reached through this: props, data, computeds and methods are all merged
onto one instance. That is convenient, and it is the source of the one bug worth warning about.
export default {
data() {
return { count: 0 };
},
methods: {
// WRONG. An arrow function takes `this` from where it was DEFINED, which
// here is the module -- so `this` is undefined and this throws.
increment: () => {
this.count++;
},
// Correct. A normal method gets `this` bound to the instance.
decrement() {
this.count--;
},
},
};Never use an arrow function for an option. The same applies to
data, computed entries and lifecycle hooks. Inside a method, arrow functions
are fine and correct — they inherit the method's this, which is what you want in a
callback.
The Composition API has no this at all, which is one of the reasons it exists.
Why this track uses the Composition API
Logic reuse. This is the real reason. The Options API's answer was mixins, which merge properties in from a distance — two mixins can overwrite each other silently, and nothing tells you where a property came from. Composables are function calls with return values. Lesson 15 is the argument.
Related code stays together. In a large Options component, one feature's state is
in data, its derived values in computed, its handlers in
methods and its setup in mounted — four places, several hundred lines apart.
Composition lets you put all four next to each other.
TypeScript. Typing this across merged option objects needs a lot of
machinery and still has gaps. Composition is plain variables and functions, so inference just
works.
Less shipped code. <script setup> compiles bindings directly
into the render function, with no instance proxy in between.
When the Options API is the right answer
Honestly: when your team already uses it. Consistency inside a codebase is worth more than either API's advantages, and a project half in each is worse than a project entirely in the one you like less.
It is also genuinely easier to start with. The structure is given to you — there is one place state goes — where a Composition component is a blank page you have to organise yourself. For a small component, that structure is a real benefit.
They also interoperate. An Options component can have a setup() that returns bindings
the rest of the options can see, and Options and Composition components can be used together in one
tree with no adapter.
What to do when you meet it
Read it. Do not convert it. A working Options component has no bug that rewriting it fixes, and the rewrite is a chance to introduce one. Write new components in the Composition API, leave the old ones alone, and only convert something you are already substantially rewriting for another reason.
Next: Routing with Vue Router.