When we work with clients at Zeitspace, one thing we're always doing is looking ahead. What is coming down the pipeline in terms of software development and what can we do now to make that transition easier? On a recent project with Kitchener Waterloo Community Foundation (KWCF) we did just that when we decided to update the code base to use the Vue 2 Composition API plugin to ease the transition to Vue 3.
The Composition API allows you to organize your code by functionality instead of it being separated into variables, methods, and computed properties. It also gets rid of the this keyword which allows you to use arrow functions without worrying about what this refers to. The Composition API also introduces composables, which allow you to easily and cleanly reuse code and don’t have the same pitfalls that the Options API’s mixins did such as naming conflicts and implicit dependencies.
Even though Vue 3 was officially launched in September 2020, a lot of major libraries have been slow to upgrade to be compatible with Vue 3. Vuetify 3, the newest version of a major Vue component library, was set to launch May 2022, (it hasn’t yet), which will add support for the Composition API allowing more people to make the switch to Vue 3. Our KWCF project uses Vuetify, which is why we didn’t just update to Vue 3 right away and instead opted for the Composition API plugin. The Composition API plugin allows you to use Vue 3’s Composition API in Vue 2 which can help ease the transition to Vue 3.
To demonstrate how we used the plugin, I created a little todo list application that we’ll convert from Vue 2's Options API to Vue 3's Composition API using the Composition API plugin. The app uses Node 16, MongoDB, and Vue. Grab the front end from here:
git clone https://github.com/JacksonZeitspace/vuedo.git
and backend here:
git clone https://github.com/JacksonZeitspace/vuedo-backend.git
To set up the back end, make sure MongoDB is running (https://www.mongodb.com/docs/manual/installation/) and then run:
cd vuedo-backend
npm i
Note: The next command will drop your todo database if you already have one. You can modify the cleanstart script to use another database name instead of todo to avoid this. You will also have to change the dbConfig property in src/config/dev.json to connect to the name you choose.
npm run cleanstart
To get started, enter the vuedo repo with cd vuedo in a new terminal instance and we’ll install the Vue composition API plugin. We’ll also be converting our vuex store so that it’s compatible with the Composition API, so we’ll install the vuex composition helpers as well:
npm i @vue/composition-api
npm i vuex-composition-helpers
We must then import and install the plugin with Vue.use before we can use the Composition API in the app. We do this by adding these lines to our main.ts file
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
Now that we’re set up, we can convert our first component to use the Composition API.
We’ll start by converting components/AddTodoModal.vue . The Composition API uses defineComponent instead of Vue.extend so we’ll start the conversion by importing it and using it to define the component.
import { defineComponent, ref } from '@vue/composition-api'
export default defineComponent({
The setup function is the entry point for Composition API use in Vue. Within it, you can define methods and variables together and then return them to the template. The setup function has two parameters, props and context . The props parameter functions almost the same way as this.props did before except it is now accessed with props.propName instead of this.propName since there is no more this keyword in the Composition API. Context is the second parameter that’s used to access other values that you may be familiar with from the Options API, such as attrs, slots, emit, and expose, that would have been accessed through this . We will use context later to access the Vue router.
We can move the variables from our data section and put them in the setup function. To keep our variables reactive, we need to wrap them in a “ref”. A ref is a reactive wrapper that has one property which can be accessed with the .value property. To do this, add a setup function within defineComponent like this:
We can move the variables from our data section and put them in the setup function. To keep our variables reactive, we need to wrap them in a “ref”. A ref is a reactive wrapper that has one property which can be accessed with the .value property. To do this, add a setup function within defineComponent like this:
export default defineComponent({
name: 'AddTodoModal',
setup() {
const addTodoModal = ref(false)
const title = ref('')
const description = ref('')
}
As you can see, you can define the default value for a ref by passing it to the ref's constructor. If you need to declare a type for an object or array variable, you can also do that while passing a default value to the ref:
const someObject = ref({} as SomeType)
If we want to be able to access these variables in the template we must also return them within the setup function:
export default defineComponent({
name: 'AddTodoModal',
setup() {
const addTodoModal = ref(false)
const title = ref('')
const description = ref('')
return {
addTodoModal,
title,
description,
}
}
})
An alternate way to declare reactive variables would be to use the “reactive” method. You can use this if you would prefer to have either all or some of your state variables be a reactive object. With a reactive object, you don’t have to define each property as an individual ref and can access the property with form.title instead of title.value . You can do this in src/views/TodoItem as an example:
const form = reactive({
title: props.title,
description: props.description
})
We’ll also update how we access the Vuex store so let's import the vuex composition helpers and create a namespaced helper for the todos store. To access the vuex actions, we will use the useActions hook. This will replace mapActions in the computed section.
import { createNamespacedHelpers } from 'vuex-composition-helpers'
const { useActions } = createNamespacedHelpers('todo')
Note: we can rename useActions to useTodoActions . This is generally good practice to increase readability and to allow adding other namespaced helpers without conflicting variable names.
const { useActions: useTodoActions } = createNamespacedHelpers('todo')
Then we can remove mapActions and replace it by adding useTodoActions within our setup method like this:
const { createTodo } = useTodoActions(['createTodo'])
to access the createTodo action.
Next we’ll move our methods from the methods section to the setup function. A benefit of having our variables and methods in the same section is it allows us to logically group them by functionality instead of grouping them by variables and methods.
To convert the methods section, copy the saveTodo method and paste it in the setup function and then delete the methods section. We need to properly define our methods by adding the function keyword after async . We also need to remove this. everywhere since it no longer exists in the Composition API and append .value when accessing the value of refs.
async function saveTodo(): Promise<void> {
await createTodo({ title: title.value, description: description.value })
addTodoModal.value = false
}
We could also define this same function as an arrow function and still access our state variables with no issue now that the this keyword is no longer used:
const saveTodo = async (): Promise<void> => {
await createTodo({ title: title.value, description: description.value })
addTodoModal.value = false
}
We also have to return the saveTodo method so that we can call it from the template.
We’re finished converting the AddTodoModal component. Let’s take a look at the views/TodoList.vue view.
Each of the lifecycle methods are also replaced with lifecycle hooks. Import the onMounted hook from @vue/composition-api and use it like this to replace the old ‘mounted’ method:
const { getTodos, toggleDone } = useTodoActions(['getTodos', 'toggleDone'])
onMounted(async (): Promise<void> => {
await getTodos()
})
Computed values function the same way as before but instead of being in their own section they are also in the setup function and use the computed hook. Just like how we mapped our data variables to refs, we can import the computed method from the Composition API plugin and use it to create computed variables. These variables' value also needs to be accessed with the .value property.
We first need to access our Vuex state variables by using the useState hook just like we accessed actions before with the useActions hook.
const { useActions: useTodoActions, useState: useTodoState } = createNamespacedHelpers('todo')
export default defineComponent({
components: {
AddTodoModal
},
setup(props, context) {
const { todos } = useTodoState(['todos'])
We can then use the computed method to compute which todos are done like this:
const doneTodos = computed(() => todos.value.filter((todo: Todo) => todo.done))
You now know everything you need to know to convert the rest of the app and can try it yourself or look at the composition-api branch for the test project on our GitHub page to see a completed version.
After converting your code base, you can take advantage of some of the powerful features of the Composition API such as composables. With a composable we could take similar logic from all three todo lists to create a single set of code they can all share. Take a look at how to use them here. As more libraries start to support Vue3's Composition API, it's a good time to start updating your code base to ease the transition and start taking advantage of some of the Composition API's powerful features, such as better code organization, easier use of arrow functions, and composables.