Vue 3 lets you write a component in two styles. The Options API organizes code into option objects (data, methods, computed), while the Composition API organizes code by feature using imported functions such as ref and computed. After this lesson you will be able to read both styles, translate between them and choose the right one for a project.
In the Options API a component is an object whose keys are fixed option names. Reactive state lives in data(), functions in methods, derived values in computed, and everything is accessed through this:
<script>
export default {
data() {
return { count: 0 }
},
computed: {
double() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('mounted with', this.count)
}
}
</script>
<template>
<button @click="increment">{{ count }} (double: {{ double }})</button>
</template>data must be a function that returns a fresh object, so every component instance gets its own copy of the state. Lifecycle hooks (created, mounted, updated, unmounted) are options too.
With <script setup> there are no option buckets. You import what you need and declare plain variables and functions:
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('mounted with', count.value))
</script>
<template>
<button @click="increment">{{ count }} (double: {{ double }})</button>
</template>Two things differ visibly. There is no this; you reference count directly. And a ref is unwrapped in the template ({{ count }}), but in script you read and write count.value.
| Options API | Composition API |
| --- | --- |
| data() | ref(), reactive() |
| computed: {} | computed() |
| methods: {} | plain functions |
| watch: {} | watch(), watchEffect() |
| mounted() | onMounted() |
| beforeUnmount() | onBeforeUnmount() |
| props option | defineProps() |
| emits option | defineEmits() |
| mixins | composables |
| this.$refs.x | template refs via useTemplateRef('x') |
Note that created and beforeCreate have no Composition API equivalent: code placed directly inside <script setup> runs at that point.
The Options API is easy to learn because it tells you where to put things. Its weakness shows in large components: the logic for one feature (say, fetching and filtering a list) is scattered across data, computed, methods and mounted. The Composition API keeps that logic together and lets you extract it into a reusable function called a composable:
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(start = 0) {
const count = ref(start)
const double = computed(() => count.value * 2)
const increment = () => count.value++
return { count, double, increment }
}Any component can now call const { count, increment } = useCounter(10). Mixins, the Options API equivalent, suffer from unclear property sources and naming collisions, which composables avoid. The Composition API also gives far better TypeScript inference, which is why the official documentation defaults to it.
<script setup> became the default.Both styles are fully supported and can be mixed in one project. You can even use a setup() function inside an Options API component to adopt the Composition API gradually.
.value in script when reading a ref: if (count > 5) compares the ref object, not the number.increment: () => this.count++ has the wrong this.reactive() objects or props, which loses reactivity (covered in the reactivity lesson).In the Composition API, how do you read the current value of `const count = ref(0)` inside `<script setup>`?
data, methods, computed) and uses this.ref, computed, watch and lifecycle functions.<script setup> is the recommended way to write Composition API components.Next lesson: Directives: v-if, v-for, v-bind, v-model — the built-in template directives that control rendering, attributes and two-way binding.