Refactoring a Component from Vue 2 Options API to Vue 3 Composition API
Vue 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()
andreactive()
, that allows us to directly create the reactive state, computed state, and watchers. - Lifecycle Hooks, e.g.
onMounted()
andonUnmounted()
, that allows us to hook into the component lifecycle programmatically. - Dependency Injection, i.e.
provide()
andinject()
, 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 andreactive
for 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
anddefineEmits
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.
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.