Refactoring a Component from Vue 2 Options API to Vue 3 Composition API

Fotis Adamakis
6 min readAug 16, 2022

VVue 3 is gaining traction, and sooner than later, every codebase will need to be migrated. It comes with many new paradigms and API changes, but the one that stands out is the Composition API. The primary advantage of Composition API is that it enables clean, efficient logic reuse in the form of Composable functions. It solves the drawbacks of mixins and enables the creation of impressive community projects such as VueUse. It also serves as a clean mechanism for easily integrating stateful third-party services or libraries into Vue’s reactivity system, for example, immutable data, state machines, and RxJS.

Even though there is still possible to write your Vue 3 components using the Options API, the strong recommendation is to use the new Composition API and sooner than later, everyone will have to get familiar with it. After several months of working with it myself, I found that the best way to get familiar is to get my hands dirty by converting Vue 2 components to Vue 3. Let’s do that together.

Reference Component

First, we need to define a reference component in Vue 2 using the options API. A lot is going on, but everything should look familiar since this is the most popular way to write Vue components since single file components were introduced several years ago.

Things to notice here that later will be refactored using Vue 3:

  • Child component declaration
  • Async component declaration
  • Declaring props
  • Declaring reactive data
  • Computed value
  • Watcher
  • Lifecycle hooks
  • Methods
  • Event emitting
  • Mixin usage

Vue 3 with Options API

First things first. Options API is not going away. It is supported in Vue 3 with an almost identical API. In order to use the component above in Vue 3, we just need to change two things:

1. Async Component Declaration

Async component declaration is made with the defineAsyncComponent helper available in the Vue core.

2. Define Emits (Optional)

Vue 3 supports declaring your emits as a component option. It is optional but recommended in order to better document how a component should work.

Composition API

Composition API is a set of utility helpers that enable us to author our components using explicitly imported functions instead of declaring options. It is an umbrella term that covers the following APIs:

  • Reactivity API, e.g. ref() and reactive(), that allows us to directly create the reactive state, computed state, and watchers.
  • Lifecycle Hooks, e.g. onMounted() and onUnmounted(), that allows us to hook into the component lifecycle programmatically.
  • Dependency Injection, i.e. provide() and inject(), that allows us to leverage Vue's dependency injection system while using Reactivity APIs.

Composition API is a built-in feature of Vue 3 and is currently available to Vue 2 via the officially maintained @vue/composition-api plugin. In Vue 3, it is also primarily used together with the <script setup> syntax in Single-File Components.

Script Setup

<script setup> is a compile-time syntactic sugar for using Composition API inside Single-File Components (SFCs). It is the recommended syntax if you are using both SFCs and Composition API. When using <script setup>, any top-level bindings (including variables, function declarations, and imports) declared inside <script setup> are directly usable in the template:

Vue 3 with Composition API

Let’s take each part of our reference SFC and adapt it, step by step, to this new paradigm.

Reactive data declaration

Reactive data are declared with the ref and reactive helpers. ref for primitives andreactivefor complex types. An optional parameter of the default value can be passed to both helpers.

Define child components

Making a component available in the template is as easy as importing it inside <script setup>. For async components, the defineAsyncComponent helper needs to be used.

Computed value

For computed, you guessed it. Another helper. Notice the absence of the this keyword inside the computed function. With the way <script setup> works, it is no longer needed.

Watchers

Watch helper accepts the variable and a callback function. Beware that watching complex types like objects is done deeply and can have a negative performance impact. You can watch only one property of an object by passing it as a second parameter. Read more about the new watch helper in the official documentation.

Lifecycle hooks

This one is a bit tricky. The mounted hook is replaced by the onMounted helper and the created hook is replaced by the setup function itself.

For reference, all the changes to the lifecycle hooks are the following:

Methods

As we already mentioned, every function declared inside the setup function will be available in the template.

Event emitting

To emit an event, first, we need to declare it with the defineEmits helper and then use the return value as the emitter.

Props

Defining component props can be done similarly. All the validation options are still supported.

ℹ️ defineProps and defineEmits are compiler macros only usable inside <script setup>. They do not need to be imported, and are compiled away when <script setup> is processed.

Mixin usage

The root of all evil* and the primary reason Composition API was introduced is to eliminate the usage of mixins. Declaring and exporting a reactive variable and a function from a file is an option, but an even better alternative is using a composable.

Imported and used like this:

Notice that useMixin is a composable, not a mixin. Naming kept consistent with the originally imported mixin.

*Personal and unpopular opinion: Mixins might not be the best way to reuse code, but they don’t deserve all the hate they get recently.

Component Name

A tiny detail is missing. Our component had a name, and declaring it inside the script setup is not possible. Thankfully multiple script tags are supported in the same file, which is the solution according to the original RFC.

Template and Styles

No changes are required in the template and style section of the component. Which, after so much refactoring, is definitely good news.

Putting everything together

Let's put everything together and see how our reference SFC has changed after refactoring it using the <script setup>

At first glance, our component looks much different, but in reality, the functionality is still the same. Everything is explicitly imported or declared, eliminating namespace collisions. Component API is clearly defined, and common functionality can be grouped together if needed.

Vue 2 Options API vs Vue 3 Composition API & script setup

We will need time to get familiar with the new API and adapt to it, but it seems that every component will look similar to this sooner than later. Probably the only difference will be that the usage of composables will grow, and more code will be abstracted to them.

What’s next?

First of all, start using Vue 3. It's ready, it's stable, and it's the future. If you are unlucky to be in the middle of a migration from Vue 2, @vue/composition-api is a good bridge to bring you closer to version 3. Migration is currently not easy or stable, but eventually, it should be. Be patient!

Another significant part that changes rapidly and we didn't mention at all in this article is state management. Pinia is the new favourable and works well with the new setup API. Read more about Pinia in the future of State management in Vue.js.

Lastly, the official documentation of the framework is once again a state of the art and has the length of a book! You can find tons of useful information there, no matter which paradigm you choose to follow.

What is your opinion about Composition API? Is our component cleaner and more maintanable? Are you excited about the future, or are you sticking with the Options API? Please leave your comment below.

--

--

« Senior Software Engineer · Author · International Speaker · Vue.js Athens Meetup Organizer »