Rewriting a Vue 2.x component with Vue Composition API
Andy G
Posted on December 29, 2019
Vue 3 will come with an additional advanced API called "Composition", which will be "a set of additive, function-based APIs that allow flexible composition of component logic."
To experiment with it and provide feedback, we can already use with Vue 2.x the @vue/composition-api plugin.
Below is a walk-through of moving from using the "standard" Vue API to the Composition API.
The component I'm going to rewrite is the following:
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<label>Enter your name: </label>
<input type="text" v-model="name" /><br>
<label>Set your age: </label>
<button type="button" @click="decreaseAge"> - </button>
<span> {{age}} </span>
<button type="button" @click="increaseAge"> + </button>
<p><small>You made {{changes}} changes to your info</small></p>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
value: String,
autoFocus: Boolean,
select: Boolean,
},
data() {
const info = this.splitInfo(this.value);
return {
...info,
changes: 0,
};
},
computed: {
personInfo() {
return `${this.normalizeName(this.name)}-${this.age}`;
},
},
watch: {
value(outsideValue) {
Object.assign(this, this.splitInfo(outsideValue));
},
personInfo() {
this.changes += 1;
this.$emit('input', this.personInfo);
},
autoFocus() {
this.setFocus();
},
select() {
this.setSelect();
},
},
mounted() {
this.setFocus();
this.setSelect();
},
methods: {
setFocus() {
if (this.autoFocus) {
this.$el.querySelector('input').focus();
}
},
setSelect() {
if (this.select) {
this.$el.querySelector('input').select();
}
},
normalizeName(name) {
return name.toUpperCase();
},
increaseAge() {
this.age += 1;
},
decreaseAge() {
this.age -= 1;
},
splitInfo(info) {
const [name, age] = info.split('-');
return { name, age: parseInt(age, 10) };
},
setChanges() {
this.changes += 1;
},
},
};
</script>
It's a "hello world" of the Vue components, accepting a v-model and a few other props. It emits an input event, changing the v-model.
Installation and setup
Install composition api:
$ npm i @vue/composition-api --save
In your main.js
add the following two lines:
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
Start with an empty setup
Add an empty setup function to the component. It is called before the beforeCreate
hook and does not have access to component instance (this
), but the properties returned from it will be exposed in the instance.
This function will be called with two parameters: props
and context
. The former being pretty self explanatory, while the latter being an object which exposes a selective list of properties that were previously exposed on this
in 2.x APIs, among which the most important are: parent
, refs
, attrs
, emit
, slots
.
Move data to reactive/refs
The model that is defined in data
can now be defined with one of the functions reactive
or ref
, depending on the use case. The first takes an object and returns a reactive proxy of it while the second takes a value and returns a reactive mutable object with a single value
property.
Moving the changes
from data to setup
:
import { ref } from '@vue/composition-api';
export default {
setup() {
const changes = ref(0);
return {
changes,
};
},
};
For the other two properties name
and age
, which are extracted from the value
prop, you need to take into consideration that we have no access to this
in setup
, hence value
needs to be taken from props
parameter and splitInfo
can to be defined outside the component info since it doesn't use the instance anyway.
import { ref, reactive, toRefs } from '@vue/composition-api';
const splitInfo = (info) => {
const [name, age] = info.split('-');
return { name, age: parseInt(age, 10) };
};
export default {
setup(props) {
// reactive properties
const changes = ref(0);
const info = reactive(splitInfo(props.value));
// return the state with the reactive properties & methods
// each property must be a ref
return {
// return properties
// changes is a ref, can be returned as such
changes,
// to convert a reactive object to a plain object with refs, use toRefs
...toRefs(info),
};
},
}
Move the computed properties
import { ref, reactive, toRefs, computed } from '@vue/composition-api';
export default {
setup(props) {
// reactive properties
const changes = ref(0);
const info = reactive(splitInfo(props.value));
// computed properties
const personInfo = computed(() => `${normalizeName(info.name)}-${info.age}`);
// return the state with the reactive properties & methods
// each property must be a ref
return {
// return properties
// changes is a ref, can be returned as such
changes,
// to convert a reactive object to a plain object with refs, use toRefs
...toRefs(info),
// return computed properties
personInfo,
};
},
}
Move the methods
Declare those that don't use the instance outside of the component declaration
const normalizeName = name => name.toUpperCase();
Declare those that use the state inside the setup
In order to have access to the reactive properties, methods that use them, need to be defined in the same scope.
setup(props) {
// reactive properties
// ...
// computed properties
// ...
// methods
const increaseAge = () => {
info.age += 1;
};
const decreaseAge = () => {
info.age -= 1;
};
const setChanges = () => {
// refs need to be accessed with the value property
changes.value += 1;
};
// return the state with the reactive properties & methods
// each property must be a ref
return {
// return properties
// ...
// return computed properties
// ...
// return methods
increaseAge,
decreaseAge,
setChanges,
};
},
this.$el
needs to be handled differently
Again, having no instance, we don't have this.$el
, but we do have refs
on the context
object passed to setup. Hence we can add a ref attribute to the root node of the component and use that
<template>
<div ref="el" />
</template>
<script>
export default {
setup(props, context) {
// reactive properties
// ...
// computed properties
// ...
// methods
// ...
const setFocus = () => {
if (props.autoFocus) {
context.refs.el.querySelector('input').focus();
}
};
const setSelect = () => {
if (props.select) {
context.refs.el.querySelector('input').select();
}
};
},
};
</script>
Move the watch functions
import {
ref, reactive, toRefs, computed, watch, onMounted,
} from '@vue/composition-api';
export default {
setup(props, context) {
// reactive properties
// ...
// computed properties
// ...
// methods
// ...
// define watches
// props, refs and reactive objects can be watched for changes
// can watch a getter function
watch(() => props.autoFocus, setFocus);
watch(() => props.select, setSelect);
// optionally, can have be lazy (won't run on component initialize)
// defaults to false, contrary to how watches work in Vue 2
watch(() => props.value, (outsideValue) => {
Object.assign(info, splitInfo(outsideValue));
}, { lazy: true });
// watch a specific ref (computed)
watch(personInfo, () => {
setChanges();
context.emit('input', personInfo.value);
});
},
};
Define lifecycle hooks
In this case, mounted
becomes onMounted
which is called in the setup
.
import {
ref, reactive, toRefs, computed, watch, onMounted,
} from '@vue/composition-api';
export default {
setup(props, context) {
// ...
// lifecycle hooks
onMounted(() => {
setFocus();
setSelect();
});
// ...
},
};
References:
Vue Composition API RFC
VueMastery Vue 3 Cheat sheet
GitHub Repo
Posted on December 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 11, 2024