Higher order components in Vue
To get the most out of this tutorial, you should have some experience with Vue.
A higher order component (HOC) is an architectural pattern that is extremely common in React but can also be used in Vue. It can be described as a method used to share common functionality between components without repeating code. The purpose of a HOC is to enhance a component’s functionality. It allows for reusability and maintainability in a project.
Whenever you pass in a component into a function and then return a new component, that’s a HOC in action.
Higher order components are very useful for:
- Manipulating props.
- Manipulating and abstracting data.
- Code reusability
Prerequisites
Before we begin the tutorial, the following bits are needed:
- Some experience with the Vue framework.
- Knowledge of setting up an application with vue-cli.
- Basic knowledge of JavaScript and Vue
- Node (8)
- npm (5.2.0)
Please ensure you have Node and npm installed before starting the tutorial.
Higher order component pattern in Vue
While higher order components are usually associated with React, it’s quite possible to create higher order components for Vue components. The pattern for creating higher order components in Vue can be seen below.
// hocomponent.js
import Vue from 'vue'
import ComponentExample from '@/components/ComponentExample.vue'
const HoComponent = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
}
}
}
const HoComponentEnhanced = HoComponent(ComponentExample);
As seen in the code block above, the HoComponent
function takes in a component as an argument and creates a new component that renders the passed component.
A basic HOC by example
In this tutorial, we’ll go through an example in which a higher order component is being used. Before we introduce higher order components, we’ll see how the current codebase works without higher order components and then figure out what to make into abstractions.
https://codesandbox.io/embed/llvq04nx4l
As seen in the CodeSandbox above, the app displays a list of paper companies and their net worth, as well as the characters from The Office (US) with their awards count.
There’s a single source of truth in which we get all the data needed for the app, which is the mockData.js
file.
// src/components/mockData.js
const staff = [
{
name: "Michael Scott",
id: 0,
awards: 2
},
{
name: "Toby Flenderson",
id: 1,
awards: 0
},
{
name: "Dwight K. Schrute",
id: 2,
awards: 10
},
{
name: "Jim Halpert",
id: 3,
awards: 1
},
{
name: "Andy Bernard",
id: 4,
awards: 0
},
{
name: "Phyllis Vance",
id: 5,
awards: 0
},
{
name: "Stanley Hudson",
id: 6,
awards: 0
}
];
const paperCompanies = [
{
id: 0,
name: "Staples",
net: 10000000
},
{
id: 1,
name: "Dundler Mufflin",
net: 5000000
},
{
id: 2,
name: "Michael Scott Paper Company",
net: 300000
},
{
id: 3,
name: "Prince Family Paper",
net: 30000
}
];
export default {
getStaff() {
return staff;
},
getCompanies() {
return paperCompanies;
},
increaseAward(id) {
staff[id].awards++;
},
decreaseAward(id) {
staff[id].awards--;
},
setNetWorth(id) {
paperCompanies[id].net = Math.random() * (5000000 - 50000) + 50000;
}
};
In the snippet above, there are const
variables that hold in the information for the companies and staff list. We are also exporting some functions that help with the following:
- returning the list of staff,
- returning the list of companies,
- increase and decrease award counts for a particular staff, and lastly
- setting the net worth of a company.
Next, let’s look at the Staff.vue
and Companies.vue
Vue components.
// src/components/Staff.vue
<template>
<main>
<h3>Staff List</h3>
<div v-for="(staff, i) in staffList" :key="i">
{{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
<button @click="increaseAwards(staff.id);">+</button>
<button @click="decreaseAwards(staff.id);">-</button>
</div>
</main>
</template>
<script>
import mockData from "./mockData.js";
export default {
data() {
return {
staffList: mockData.getStaff()
};
},
methods: {
increaseAwards(id) {
mockData.increaseAward(id);
this.staffList = mockData.getStaff();
},
decreaseAwards(id) {
mockData.decreaseAward(id);
this.staffList = mockData.getStaff();
}
}
};
</script>
In the code block above, the data instance variable staffList
is set to the content of the returned function mockData.getStaff()
. We also have the increaseAwards
and decreaseAwards
functions which call the mockData.increaseAward
and mockData.decreaseAward
respectively. The id
being passed to these functions is gotten from the rendered template.
// src/components/Companies.vue
<template>
<main>
<h3>Paper Companies</h3>
<div v-for="(companies, i) in companies" :key="i">
{{ companies.name }} - ${{ companies.net
}}<button @click="setWorth(companies.id);">Set Company Value</button>
</div>
</main>
</template>
<script>
import mockData from "./mockData.js";
export default {
data() {
return {
companies: mockData.getCompanies()
};
},
methods: {
setWorth(id) {
mockData.setNetWorth(id);
this.companies = mockData.getCompanies();
}
}
};
</script>
In the code block above, the data instance variable companies
is set to the content of the returned function mockData.getCompanies()
. We also have the setWorth
function which sets a random value as the net worth by passing the id
of the company to the mockData.setNetWorth
function. The id
being passed to the functions is gotten from the rendered template.
Now that we’ve seen how both components work, we can figure out what’s common between them and what we can turn into abstractions and they are as follows:
- fetching data from a source of truth (mockData.js)
- onClick functions
Let’s see how to put the actions above into a higher order component so as to avoid code repetition and ensure reusability.
You can go ahead to create a new Vue project by using the vue-cli package. Vue CLI is a full system for rapid Vue.js development, it ensures you have a working development environment with no need for build configs. You can install vue-cli
with the command below.
npm install -g @vue/cli
Once the installation is done, you can go ahead to create a new project with the command below where vue-hocomponent
is the name of the application. Make sure to choose the default preset as there won’t be a need to customise the options.
vue create vue-hocomponent
With installation done, you can go ahead to create the following files and then edit with the content of the snippets shared above.
- A
Staff.vue file
in thesrc/components
folder. - A
Companies.vue
file in thesrc/components
folder. - A
mockData.js
file in thesrc/components
folder.
Alternatively, you can just fork the CodeSandbox app to follow the tutorial.
Next step is to create a higher order component file that will be used for abstractions. Create a file in the src
folder named HoComponent.js
and edit with the following.
// src/components/HoComponent.js
import Vue from "vue";
import mockData from "./mockData.js";
const HoComponent = (component, fetchData) => {
return Vue.component("HoComponent", {
render(createElement, context) {
return createElement(component, {
props: {
returnedData: this.returnedData
}
});
},
data() {
return {
returnedData: fetchData(mockData)
};
}
});
};
export default HoComponent;
In the code block above, Vue is imported as well as the data from the mockData
file.
The HoComponent
function accepts two arguments, a component and fetchData
. The fetchData
method is used to determine what to display in the presentational component. That means whatever function is passed as fetchData
wherever the higher order component is being used, is used to actually get data from mockData
.
The data instance returnedData
is then set to the content of fetchData
and subsequently passed as a props
to the new component created in the higher order component.
Let’s see how the newly created higher order component can be used in the app. We’ll need to edit both the Staff.vue
and Companies.vue
.
// src/components/Staff.vue
<template>
<main>
<h3>Staff List</h3>
<div v-for="(staff, i) in returnedData" :key="i">
{{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
<button @click="increaseAwards(staff.id);">+</button>
<button @click="decreaseAwards(staff.id);">-</button>
</div>
</main>
</template>
<script>
export default {
props: ["returnedData"]
};
</script>
// src/components/Companies.vue
<template>
<main>
<h3>Paper Companies</h3>
<div v-for="(companies, i) in returnedData" :key="i">
{{ companies.name }} - ${{ companies.net
}}<button @click="setWorth(companies.id);">Set Company Value</button>
</div>
</main>
</template>
<script>
export default {
props: ["returnedData"]
};
</script>
As you can see in the code block above, for both components, we’ve taken away the functions and data instance variables, all the data needed to display content will now be gotten from the props. For the functions removed, we’ll treat that soon.
In the App.vue
component, edit the existing content in the script
tag with the following.
// src/App.vue
<script>
// import the Companies component
import Companies from "./components/Companies";
// import the Staff component
import Staff from "./components/Staff";
// import the higher order component
import HoComponent from "./components/HoComponent.js";
// Create a const variable which contains the Companies component wrapped in the higher order component
const CompaniesComponent = HoComponent(Companies, mockData => mockData.getCompanies()
);
// Create a const variable which contains the Staff component wrapped in the higher order component
const StaffComponent = HoComponent(Staff, mockData => mockData.getStaff());
export default {
name: "App",
components: {
CompaniesComponent,
StaffComponent
}
};
</script>
In the code block above, the HoComponent
is used to wrap both the Staff
and Companies
Vue components. Each component is passed in as the first argument of the HoComponent
and the second argument is a function that returns another function specifying what data should be fetched from mockData
. This is the fetchData
function in the higher order component (HoComponent.js) we created earlier.
If you refresh the app now, you should still see data from the mockData
file being rendered as usual. The only difference is, the buttons will not work because they are not hooked to any function yet. Let’s address that.
We’ll start by making some modifications to both files, Staff.vue
and Companies.vue
.
// src/components/Staff.vue
<template>
<main>
<h3>Staff List</h3>
<div v-for="(staff, i) in returnedData" :key="i">
{{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
<button @click="$emit('click', { name: 'increaseAward', id: staff.id });">
+
</button>
<button @click="$emit('click', { name: 'decreaseAward', id: staff.id });">
-
</button>
</div>
</main>
</template>
<script>
export default {
props: ["returnedData"]
};
</script>
// src/components/Companies.vue
<template>
<main>
<h3>Paper Companies</h3>
<div v-for="(companies, i) in returnedData" :key="i">
{{ companies.name }} - ${{ companies.net
}}<button
@click="$emit('click', { name: 'setNetWorth', id: companies.id });"
>
Set Company Value
</button>
</div>
</main>
</template>
<script>
export default {
props: ["returnedData"]
};
</script>
In both code snippets above, we are emitting events which will be used in the parent component, App.vue
. We are emitting an object, which contains to values, the name of the function associated with the action we’re trying to execute and the corresponding id
of what’s being clicked on. Don’t forget that the increaseAward
, decreaseAward
and setNetWorth
functions are defined in the mockData.js
file.
Next, we’ll edit the parent component, App.vue
to act on what’s being emitted from the child component. Make the edits below to your App.vue
file.
// src/App.vue
<template>
<div id="app">
<CompaniesComponent @click="onEventHappen" />
<StaffComponent @click="onEventHappen" />
</div>
</template>
<script>
// import the Companies component
import Companies from "./components/Companies";
// import the Staff component
import Staff from "./components/Staff";
// import the higher order component
import HoComponent from "./components/HoComponent.js";
// import the source data from mockData only to be used for event handlers
import sourceData from "./components/mockData.js";
// Create a const variable which contains the Companies component wrapped in the higher order component
const CompaniesComponent = HoComponent(Companies, mockData =>
mockData.getCompanies()
);
// Create a const variable which contains the Staff component wrapped in the higher order component
const StaffComponent = HoComponent(Staff, mockData => mockData.getStaff());
export default {
name: "App",
components: {
CompaniesComponent,
StaffComponent
},
methods: {
onEventHappen(value) {
// set the variable setFunction to the name of the function that was passed iin the value emitted from child component i.e. if value.name is 'increaseAward', setFunction is set to increaseAward()
let setFunction = sourceData[value.name];
// call the corresponding function with the id passed as an argument.
setFunction(value.id);
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
In the code block above, we added an event listener inside App.vue
component. The onEventHappen
method is called whenever there’s a click on either of these components, Staff.vue
and Companies.vue
.
In the onEventHappen
method, we set the variable setFunction
to the name of the function that was passed in the value emitted from child component i.e. if value.name
is ‘increaseAward’, then setFunction
is set to increaseAward()
. setFunction
is then run with the id passed as an argument.
Finally, in order to pass down event listeners to components wrapped in a higher order component, we’d need to add the line of code below in the HoComponent.js
file.
props: {
returnedData: this.returnedData
},
on: { ...this.$listeners }
You can refresh the app now, and all the buttons should work as expected.
vue-hoc
Alternatively, you can use the vue-hoc library which helps to create higher order components. vue-hoc helps you create higher order components easily, all you have to do is pass in the base component, a set of component options to apply to the HOC, and a set of data properties to pass to the component during render.
vue-hoc can be installed with the command below.
npm install --save vue-hoc
The vue-hoc plugin has examples that can get you started with creating higher order components which you can check here.
Conclusion
In this tutorial, we established that the primary use of higher order components is to enhance the reusability and logic of presentational components in your app.
We also established that higher order components are useful for the following:
- Manipulating props.
- Manipulating and abstracting data.
- Code reusability
We then went ahead to look at an example on how to create and use higher order components in Vue. The codebase for the Vue app above can be viewed on GitHub.
13 December 2018
by Yomi Eluwande