Higher order components in Vue

Introduction

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.

1// hocomponent.js
2    import Vue from 'vue'
3    import ComponentExample from '@/components/ComponentExample.vue'
4    
5    const HoComponent = (component) => {
6      return Vue.component('withSubscription', {
7        render(createElement) {
8          return createElement(component)
9        } 
10      }
11    }
12    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.

1// src/components/mockData.js
2    const staff = [
3        {
4          name: "Michael Scott",
5          id: 0,
6          awards: 2
7        },
8        {
9          name: "Toby Flenderson",
10          id: 1,
11          awards: 0
12        },
13        {
14          name: "Dwight K. Schrute",
15          id: 2,
16          awards: 10
17        },
18        {
19          name: "Jim Halpert",
20          id: 3,
21          awards: 1
22        },
23        {
24          name: "Andy Bernard",
25          id: 4,
26          awards: 0
27        },
28        {
29          name: "Phyllis Vance",
30          id: 5,
31          awards: 0
32        },
33        {
34          name: "Stanley Hudson",
35          id: 6,
36          awards: 0
37        }
38    ];
39    const paperCompanies = [
40      {
41        id: 0,
42        name: "Staples",
43        net: 10000000
44      },
45      {
46        id: 1,
47        name: "Dundler Mufflin",
48        net: 5000000
49      },
50      {
51        id: 2,
52        name: "Michael Scott Paper Company",
53        net: 300000
54      },
55      {
56        id: 3,
57        name: "Prince Family Paper",
58        net: 30000
59      }
60    ];
61    
62    export default {
63      getStaff() {
64        return staff;
65      },
66      getCompanies() {
67        return paperCompanies;
68      },
69      increaseAward(id) {
70        staff[id].awards++;
71      },
72      decreaseAward(id) {
73        staff[id].awards--;
74      },
75      setNetWorth(id) {
76        paperCompanies[id].net = Math.random() * (5000000 - 50000) + 50000;
77      }
78    };

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.

1// src/components/Staff.vue
2    <template>
3      <main>
4        <h3>Staff List</h3>
5        <div v-for="(staff, i) in staffList" :key="i">
6          {{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
7          <button @click="increaseAwards(staff.id);">+</button>
8          <button @click="decreaseAwards(staff.id);">-</button>
9        </div>
10      </main>
11    </template>
12    <script>
13      import mockData from "./mockData.js";
14      export default {
15        data() {
16          return {
17            staffList: mockData.getStaff()
18          };
19        },
20        methods: {
21          increaseAwards(id) {
22            mockData.increaseAward(id);
23            this.staffList = mockData.getStaff();
24          },
25          decreaseAwards(id) {
26            mockData.decreaseAward(id);
27            this.staffList = mockData.getStaff();
28          }
29        }
30      };
31    </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.

1// src/components/Companies.vue
2    <template>
3      <main>
4        <h3>Paper Companies</h3>
5        <div v-for="(companies, i) in companies" :key="i">
6          {{ companies.name }} - ${{ companies.net
7          }}<button @click="setWorth(companies.id);">Set Company Value</button>
8        </div>
9      </main>
10    </template>
11    
12    <script>
13      import mockData from "./mockData.js";
14        export default {
15        data() {
16          return {
17            companies: mockData.getCompanies()
18          };
19        },
20        methods: {
21          setWorth(id) {
22            mockData.setNetWorth(id);
23            this.companies = mockData.getCompanies();
24          }
25        }
26      };
27    </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 the src/components folder.
  • A Companies.vue file in the src/components folder.
  • A mockData.js file in the src/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.

1// src/components/HoComponent.js
2    import Vue from "vue";
3    import mockData from "./mockData.js";
4    
5    const HoComponent = (component, fetchData) => {
6      return Vue.component("HoComponent", {
7        render(createElement, context) {
8          return createElement(component, {
9            props: {
10              returnedData: this.returnedData
11            }
12          });
13        },
14        data() {
15          return {
16            returnedData: fetchData(mockData)
17          };
18        }
19      });
20    };
21    
22    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.

1// src/components/Staff.vue
2    <template>
3      <main>
4        <h3>Staff List</h3>
5        <div v-for="(staff, i) in returnedData" :key="i">
6          {{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
7          <button @click="increaseAwards(staff.id);">+</button>
8          <button @click="decreaseAwards(staff.id);">-</button>
9        </div>
10      </main>
11    </template>
12    <script>
13      export default {
14        props: ["returnedData"]
15      };
16    </script>
1// src/components/Companies.vue
2    <template>
3      <main>
4        <h3>Paper Companies</h3>
5        <div v-for="(companies, i) in returnedData" :key="i">
6          {{ companies.name }} - ${{ companies.net
7          }}<button @click="setWorth(companies.id);">Set Company Value</button>
8        </div>
9      </main>
10    </template>
11    <script>
12      export default {
13        props: ["returnedData"]
14      };
15    </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.

1// src/App.vue
2    <script>
3      // import the Companies component
4      import Companies from "./components/Companies";
5      // import the Staff component
6      import Staff from "./components/Staff";
7      // import the higher order component
8      import HoComponent from "./components/HoComponent.js";
9      
10      // Create a const variable which contains the Companies component wrapped in the higher order component
11      const CompaniesComponent = HoComponent(Companies, mockData => mockData.getCompanies()
12      );
13      
14      // Create a const variable which contains the Staff component wrapped in the higher order component
15      const StaffComponent = HoComponent(Staff, mockData => mockData.getStaff());
16      
17      export default {
18        name: "App",
19        components: {
20          CompaniesComponent,
21          StaffComponent
22        }
23      };
24    </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.

1// src/components/Staff.vue
2    <template>
3      <main>
4        <h3>Staff List</h3>
5        <div v-for="(staff, i) in returnedData" :key="i">
6          {{ staff.name }}: {{ staff.awards }} Salesman of the year Award 🎉
7          <button @click="$emit('click', { name: 'increaseAward', id: staff.id });">
8          +
9          </button>
10          <button @click="$emit('click', { name: 'decreaseAward', id: staff.id });">
11          -
12          </button>
13        </div>
14      </main>
15    </template>
16    <script>
17      export default {
18        props: ["returnedData"]
19      };
20    </script>
1// src/components/Companies.vue
2    <template>
3      <main>
4        <h3>Paper Companies</h3>
5        <div v-for="(companies, i) in returnedData" :key="i">
6          {{ companies.name }} - ${{ companies.net
7          }}<button
8          @click="$emit('click', { name: 'setNetWorth', id: companies.id });"
9          >
10          Set Company Value
11          </button>
12        </div>
13      </main>
14    </template>
15    <script>
16      export default {
17        props: ["returnedData"]
18      };
19    </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.

1// src/App.vue
2    <template>
3      <div id="app">
4        <CompaniesComponent @click="onEventHappen" />
5        <StaffComponent @click="onEventHappen" />
6      </div>
7    </template>
8    
9    <script>
10      // import the Companies component
11      import Companies from "./components/Companies";
12      // import the Staff component
13      import Staff from "./components/Staff";
14      // import the higher order component
15      import HoComponent from "./components/HoComponent.js";
16      // import the source data from mockData only to be used for event handlers
17      import sourceData from "./components/mockData.js";
18      // Create a const variable which contains the Companies component wrapped in the higher order component
19      const CompaniesComponent = HoComponent(Companies, mockData =>
20      mockData.getCompanies()
21      );
22      // Create a const variable which contains the Staff component wrapped in the higher order component
23      const StaffComponent = HoComponent(Staff, mockData => mockData.getStaff());
24    
25      export default {
26        name: "App",
27        components: {
28          CompaniesComponent,
29          StaffComponent
30          },
31        methods: {
32          onEventHappen(value) {
33            // 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()
34            let setFunction = sourceData[value.name];
35            // call the corresponding function with the id passed as an argument.
36            setFunction(value.id);
37          }
38        }
39      };
40    </script>
41    
42    <style>
43    #app {
44      font-family: "Avenir", Helvetica, Arial, sans-serif;
45      -webkit-font-smoothing: antialiased;
46      -moz-osx-font-smoothing: grayscale;
47      text-align: center;
48      color: #2c3e50;
49      margin-top: 60px;
50    }
51    </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.

1props: {
2    returnedData: this.returnedData
3    },
4    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.