Reading Options API code
Read and extend components written in the older Options API, map each option and this.$ property to its Composition API twin, avoid the arrow-function trap, and migrate one component at a time.
What you'll learn
When you finish, you can do these.- 1Read an Options API component, section by section
- 2Rewrite an Options API component with script setup
- 3Move an old app to script setup one component at a time
- 4Fix a method that fails as an arrow function
You join a team whose app was started years ago, and your first task asks for a Reset button on a counter. The counter's file has no <script setup>: it is one object with sections such as data() for its values and methods for its functions, all reached through the word this. That style is the Options API, Vue's original way to write components, still supported in Vue 3 and common in older projects and libraries.
The Code tab holds that counter twice: Counter.vue in the Options API, and CounterSetup.vue with <script setup>. By the end you can read the first, match it piece by piece to the second, and add the Reset button to both. You need one idea from Render functions and JSX: a component can be a plain object handed to defineComponent(), with its props and a setup() function inside.
One object, one section per kind of thing
An Options API component is one object, with a named section for each kind of thing. The counter from the task is a button that counts up from a start number its parent passes in, with a status line beside it. This is the script block of Counter.vue in the Code tab:
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'OptionsCounter',
props: { start: { type: Number, default: 0 } },
emits: ['change'],
data() {
return { count: this.start, status: 'not mounted yet' }
},
computed: {
double(): number {
return this.count * 2
},
},
watch: {
count(value: number) {
this.$emit('change', value)
},
},
mounted() {
this.status = 'mounted'
},
methods: {
increment() {
this.count++
},
},
})
</script>
When its parent passes a start of 5, the button reads Options: 5 (double 10) and the status beside it says mounted. Each click adds one.
props declares start, and emits declares change, the event the counter sends out. data() returns the state, and Vue makes it reactive. computed holds getters, functions that work out a value from the state, such as double. watch holds watchers, functions that run when the value they are named after changes. methods holds the functions the template calls. Lifecycle hooks have fixed names: mounted runs once the component is on the page. TypeScript needs the : number on double to type this.
Inside every section, this is the component. this.start reads a prop, this.count reads state, this.double reads a computed value and this.increment() calls a method. The template uses the same names without this.
Properties that start with $ come from Vue itself: this.$emit() sends an event, this.$refs.input is the element marked ref="input" in the template, and this.$nextTick() waits for the page to update.
The same counter with script setup
Every option has a twin in the Composition API, the style of <script setup>, where a component is built from functions such as ref() and watch() instead of options. Vue's $ properties have twins too, and so do mixins, the Options API's way to share logic between components.
Options APIdata() · computed: { double() {} } · methods: { increment() {} } · watch: { count(v) {} } · mounted() · this.$emit('change') · this.$refs.input · this.$nextTick() · mixins
Composition APIref() / reactive() · computed(() => …) · plain functions · watch(count, v => …) · onMounted() · defineEmits() · useTemplateRef('input') · nextTick() · composables
CounterSetup.vue is the same counter written that way. Its script starts:
defineOptions({ name: 'SetupCounter' })
const { start = 0 } = defineProps<{ start?: number }>()
const emit = defineEmits<{ change: [value: number] }>()
const count = ref(start)
const double = computed(() => count.value * 2)
watch(count, value => emit('change', value))
Two options have no function of their own: name, shown in Vue's DevTools, and inheritAttrs: false, which keeps a parent's extra attributes, the ones that are not props, off the root element. Since Vue 3.3, defineOptions() sets both, as on the first line.
defineProps() declares start, 0 by default, and ref(start) makes count begin at its value. this.$emit() becomes emit, the function defineEmits() hands back.
App.vue, the parent of both counters, shows them with a start of 5, and under them a line naming the counter that sent change last. Both status lines read mounted. Click the setup counter, and the line reads Composition API counter is 6.
Migrating one piece at a time
Both styles can live in one project, so you convert a component when you next touch it. An Options component can even hold a setup() option, like the defineComponent() objects in Render functions and JSX. Whatever it returns is available on this and in the template.
That lets an old component use a composable, a function that holds reusable logic and its state, before the rest of it moves. For a search composable called useSearch(), the option is setup() { return useSearch() }. Each mixin becomes a composable too, which shows where each value comes from.
When no Options API code is left, drop Vue's Options API support from the bundle, the JavaScript the browser downloads. In vite.config.ts, the settings file of the build tool Vite, change vue() to vue({ features: { optionsAPI: false } }), which sets Vue's __VUE_OPTIONS_API__ flag to false. Check your libraries first, because their Options components stop working too.
What goes wrong
The first mistake is an arrow function, the short () => {} form, in methods. It has no this of its own.
methods: {
increment: () => {
this.count++
},
},
Click the button: the number does not move, and the preview shows Cannot read properties of undefined (reading 'count'). An arrow function keeps the this of the code around it: the file itself, where it is undefined. Vue can only point this at the component for ordinary functions. The same goes for computed, watch and the hooks.
methods: {
increment() {
this.count++
},
},
With an ordinary function, every click counts up again.
The second trap is translating a watcher word for word. The counter cannot show it, so picture a profile form that marks itself unsaved whenever the user's name changes. Written as a component, it would be one file, Profile.vue, not in the Code tab, with this Options watcher on a path written as a string:
watch: {
'user.name'(name: string) {
this.saved = false
},
},
Moved to <script setup> word for word, with user as a reactive() object and saved as a ref, it becomes:
const user = reactive({ name: 'Asha' })
watch(user.name, () => { saved.value = false })
The callback never runs. Vue warns Invalid watch source: with the value Asha, then A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. Reading user.name hands watch a plain string, which it cannot track.
const user = reactive({ name: 'Asha' })
watch(() => user.name, () => { saved.value = false })
A getter, a small function that reads user.name again each time Vue checks, lets the watcher see every change.
What to remember
- An Options API component is one object. Inside it,
thisreaches props, state, computed values, methods and Vue's$properties. - Every option has a Composition API twin. A string watch path such as
'user.name'becomes a getter,() => user.name, anddefineOptions()coversnameandinheritAttrs. - Migrate a component at a time. A
setup()option lets an old component use composables before the rest of it moves. - Write methods, computed getters, watchers and hooks as ordinary functions,
increment() {}, never as arrow functions.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Add a Reset button to both counters:
reset() { this.count = this.start }in themethodsofCounter.vue,function reset() { count.value = start }inCounterSetup.vue, and<button @click="reset">Reset</button>in both templates. Click a counter up, then Reset: it shows 5 again, and the line under the counters says that counter is 5. - Add
tripleto both:triple(): number { return this.count * 3 }in thecomputedofCounter.vue, andconst triple = computed(() => count.value * 3)inCounterSetup.vue. Showtriple {{ triple }}in both buttons: both start at triple 15, and each click adds 3. - In
Counter.vue, changeincrement() {toincrement: () => {. Click the Options counter: the number stays at 5 and the preview showsCannot read properties of undefined (reading 'count'). Change it back and the click works again.