The Composition API is a great extension to Vue and the classic Options API that you are used to. It is especially useful for making code reusable and enhancing the readability of big components, but can be used for any component. Even if you haven't yet upgraded to Vue 3, you can use the plugin for Vue 2.
In this tutorial, we will extend the classic Vue Hello World
-counter-example by displaying the current time to the user and updating it every second. We will start by using the Options API, to which we are accustomed, then refactor our code to use the Composition API, and finally extract a reusable composition function.
Creating a Vue 3 app
The simplest way to test out Vue 3 is by using the tool Vite. Run
yarn create vite-app composition-api-example
cd composition-api-example
yarn
yarn dev
and visit your local server. You should see the Hello World page with a counter which increases when clicked. The entrypoint for Vue is src/Vue.app
, which renders the Vue logo and src/components/HelloWorld.vue
. We will extend the HelloWorld.vue
-component while keeping the code for the counter.
Building a clock with the Options API
Storing and displaying the current time
We start by implementing our clock with the classic Options API.
We want to display the current time to the user and will use a javascript Date
-object for this. We extend the object returned from the data()
-option with a property called currentTime
:
# ...
data() {
return {
count: 0,
currentTime: new Date(),
};
},
# ...
To actually display the time, we add a paragraph at the bottom of our template, just above the closing </template>
-tag:
<template>
# ...
<p>{{ currentTime.toLocaleString() }}</p>
</template>
After saving the component, the page should reload automatically and now display the current time.
Updating the current time
The displayed time does not yet get updated - we would need to refresh the page for that. Instead, we want to automatically update the currentTime
property from our data
every second. To do so, we add the following code to our script
-section, just beneath our data
-option:
# ...
methods: {
updateCurrentTime() {
this.currentTime = new Date();
},
},
created() {
this.intervalHandle = setInterval(this.updateCurrentTime, 1000);
},
beforeUnmount() {
clearInterval(this.intervalHandle);
},
# ...
Let's look at what we have done here:
- The
methods
-option now contains a methodupdateCurrentTime
, which just sets thecurrentTime
-property from ourdata
to the current time when called. - In the
created
-option, we set an interval which callsupdateCurrentTime
every second. We store the returned handle inthis.intervalHandle
so that we can access it later. - In the
beforeUnmount
-option (beforeDestroy
in Vue 2) we clear the interval using the stored handle.
We can now watch our clock get updated, and are already done with the Options API-implementation!
Refactor to the Composition API
Setting up our auto-updating clock required quite some code, which is also placed all over the component - we needed to use the data
, methods
, created
and beforeUnmount
options. The data
-option already contains code for two functionalities, the counter and the clock, and we can easily imagine that other options will also grow to contain code for different purposes. The Composition API offers a way to group our code by functionality instead of by option. To take advantage of this, we first need to move our code from the different options to the new setup
-function, which we will do option by option:
data
We start by moving the data
-option to the setup
-function:
import { ref } from 'vue';
export default {
# ...
setup() {
const count = ref(0);
const currentTime = ref(new Date());
return { count, currentTime };
},
# ...
}
Here we create a mutable property, called ref
, for everything we previously returned from data
. These references can be tracked by Vue's reactivity system, so that changes are automatically picked up.
We make these references, count
and currentTime
, available to the component by returning them from the setup
-function, meaning they can be accessed by using this.count
and this.currentTime
, just like when we returned them from data
.
Inside the setup
-function, we access and set the value of a reference by using .value
, e.g. currentTime.value = new Date()
or const newDate = currentTime.value
. Outside of setup
, e.g. in methods
or the <template>
-section, we need to omit the .value
. We therefore do not need to change the implementation of updateCurrentTime
, it will stay at this.currentTime = new Date()
.
methods
Next we will move the methods
-option to the setup
-function:
setup() {
const count = ref(0);
const currentTime = ref(new Date());
const updateCurrentTime = () => { currentTime.value = new Date(); };
return { count, currentTime, updateCurrentTime };
},
We just declare updateCurrentTime
as an arrow function, and return it, so that the created
-option still has access to this.updateCurrentTime
. In the function, however, we just use currentTime
instead of this.currentTime
, and we need to assign the new date to currentTime.value
.
created
Everything that happens in the created
lifecycle hook can just be written inside our setup
-function, as created
is called just after setup
:
setup() {
const count = ref(0);
const currentTime = ref(new Date());
const updateCurrentTime = () => { currentTime.value = new Date(); };
const intervalHandle = setInterval(updateCurrentTime, 1000);
return { count, currentTime, intervalHandle };
},
We just set the interval in our setup
-function, and return the intervalHandle
as a plain integer, as it is still used by the beforeUnmount
option. Note that there is no need to make a ref
out of it, as it will never change and we therefore do not need reactivity. Additionally, we removed updateCurrentTime
from the object returned from setup
, as it is now only being used inside that function.
beforeUnmount
The last remaining option is the beforeUmount
lifecycle-hook, which clears the interval. We replace it by using the onBeforeUnmount
-function which we add to the import from 'vue'
, so that our whole script-section looks like this:
import { onBeforeUnmount, ref } from 'vue';
export default {
name: 'HelloWorld',
props: {
msg: String
},
setup() {
// Counter
const count = ref(0);
// Current time
const currentTime = ref(new Date());
const updateCurrentTime = () => { currentTime.value = new Date(); };
const intervalHandle = setInterval(updateCurrentTime, 1000);
onBeforeUnmount(() => { clearInterval(intervalHandle); });
return { count, currentTime };
},
}
We removed intervalHandle
from the returned object, as we only need it within setup
- just as we did with the updateCurrentTime
function. This already shows a great benefit of the Composition API - we do not pollute our this.
with internal variables which are not meant to be used in the template.
Making our code reusable
Now that all code lives in setup
, we can strive to further organize it. In the last code snippet, we already divided our code into two logical groups - the counter and the current time sections.
Extracting composition functions
We can take these logical blocks and move them into so-called composition functions. We start by creating src/composables/useCurrentTime.js
with the following contents:
import { onBeforeUnmount, ref } from "vue";
export const useCurrentTime = () => {
const currentTime = ref(new Date());
const updateCurrentTime = () => { currentTime.value = new Date(); };
const intervalHandle = setInterval(updateCurrentTime, 1000);
onBeforeUnmount(() => clearInterval(intervalHandle.value));
return { currentTime };
};
We just copy and pasted the contents from the current time-section of the setup function, and added the neccessary imports. We can now refactor the script-section of our vue component to the following:
import { ref } from 'vue';
import { useCurrentTime } from '../composables/useCurrentTime';
export default {
name: 'HelloWorld',
props: {
msg: String
},
setup() {
const count = ref(0);
const { currentTime } = useCurrentTime();
return { count, currentTime };
},
}
That was easy! We could make the setup even shorter by returning { count, ...useCurrentTime() }
, but the explicit style makes it easier to find where things are coming from.
Adding parameters
The interval duration of 1,000 ms is currently hardcoded. However, we can easily make a parameter out of it. This way, we can use our composition in different places, even if we sometimes want to update every second and sometimes only once per hour. Simply add a parameter to our useCurrentTime
-function, and adjust the call in our component accordingly:
// HelloWorld.vue
const { currentTime } = useCurrentTime(1000);
// useCurrentTime.js
export const useCurrentTime = (refreshInterval) => {
# ...
const intervalHandle = setInterval(updateCurrentTime, refreshInterval);
# ...
}
Further improvements
There are, of course, more things we could do to extend and improve our component:
- Return
currentTime
fromuseCurrentTime
as readonly. - Additionally return
updateCurrentTime
fromuseCurrentTime
and enable the user to manually update the time in theHelloWorld
-component by clicking a button. - Extract a composition function for the counter, which could include a
increment
-function which replaces the@click="count++"
code which currently lives in the template. - Allow the
refreshInterval
-parameter ofuseCurrentTime
to be modified at any time by making it aref
. Detecting changes for that parameter is possible with the watch function, and the update frequency can be changed by usingclearInterval
andsetInterval
.
Summary
We learned how to refactor existing Vue code from the Options- to the Composition-API, step by step. Doing so helped us move the code into one place instead of scattering it all over the component,and then grouping it into logical blocks. Finally, we were able to extract a composition function which contains a part of our component, and which can easily be used again in another component.