Higher Order Components in Vue.js

As described in React’s documentation, Higher Order Component is a function which takes as an argument a component and returns newly created component. Returned component is usually augmented with features provided by HOC (Higher Order Component). Higher Order Component is not a piece of software which can be grabbed and installed, it’s a technique which may be helpful in writing reusable and maintainable code.

This post describes how to create a Higher Order Component in Vue.js base on the example that is provided in React’s documentation.


Step 1. Components Setup

The artificial application I created for the purpose of this article consists of two components: CommentsList and BlogPost. Both components are rendered inside App component, the main component of the application.

# App.vue
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>

<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
export default {
name: 'app',
components: {
'blog-post': BlogPost,
'comments-list': CommentsList
}
}
</script>

CommentsList component displays list of comments fetched from the external data source. Additionally, on mounted hook an event listener is added which listens to changes in the data source and updates comment’s list accordingly. On hook beforeDestroy the listener is removed.

# components/CommentsList.vue
<template>
<ul>
<li
v-for="(comment, index) in comments"
:key="index"
>{{comment}}</li>
</ul>
</template>

<script>
import DataSource from '../store/source.js'

export default {
name: 'comments-list',
data() {
return {
comments: DataSource.getComments()
}
},
methods: {
handleChange() {
this.comments = DataSource.getComments()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>

BlogPost component displays a blog post content. Similarly as CommentsList , it fetches its data from the external data source and updates the post’s content on every change in external data source.

# components/BlogPost.vue
<template>
<div>
{{blogPost}}
</div>
</template>

<script>
import DataSource from '../store/source.js'

export default {
data() {
return {
blogPost: DataSource.getBlogPost()
}
},
methods: {
handleChange() {
this.blogPost = DataSource.getBlogPost()
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
}
</script>

BlogPost and CommentsList components share four functionalities:

  1. Fetch the data from the external data source (in this case from DataSource ) inside mounted hook
  2. Update the data on every update in the external data source
  3. Add the change listener to the data source
  4. Remove the change listener from the data source

In order to avoid code repetitions the shared logic between BlogPost and CommentsList can be extracted to Higher Order Component.

Step 2. Higher Order Component

In this step I’ll move the duplicated code to Higher Order Component called withSubscription .

Higher Order Component is a function that takes a component as an argument and returns a new component. Let’s write it in Vue

# hocs/withSubscription.js
import Vue from 'vue'
import CommentsList from '~/components/CommentsList.vue'
const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
}
}
}
const CommentsListWithSubscription = withSubscription(CommentsList)

At this point Higher Order Component doesn’t do much. It simply takes a component and creates a new component that renders the passed component.

Next step is to implement shared logic in it. I need to add mounted , beforeDestroy hooks and handleChange method which will be called upon every update.

# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component) => {
return Vue.component('withSubscription', {
render(createElement) {
return createElement(component)
},
methods: {
handleChange() {
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}

export default withSubscription

Now the new component returned by the Higher Order Component has required lifecycle hooks. The handleChange method is left empty. Both components havehandleChange method, however, this method has slightly different implementation in each component.

Higher Order Component can accept more than one argument. Currently, withSubscription accepts only component as an argument. In order to call custom logic inside handleChange second argument is needed. The second argument is the method which should be called on every data source change. Passed method returns updated data which has to be passed down to the newly created component as a prop.

# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component, selectData) => {
return Vue.component('withSubscription', {
render(createElement, context) {
return createElement(component, {
props: {
content: this.fetchedData
}
})
},
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}

export default withSubscription

The usage of Higher Order Component inside App.vue looks as follows

# App.vue
<template>
<div id="app">
<blog-post/>
<comments-list/>
</div>
</template>

<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource) => {
return DataSource.getBlogPost()
})
const CommentsListWithSubscription = withSubscription(CommentsList, (DataSource) => DataSource.getComments())

export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription,
'comments-list': CommentsListWithSubscription
}
}
</script>

And here is the code of BlogPost and CommentsList

# components/BlogPost.vue
<template>
<div>
{{content}}
</div>
</template>

<script>
export default {
props: ['content']
}
</script>
----
# components/CommentsList.vue
<template>
<ul>
<li v-for="(comment, index) in content" :key="index">{{comment}}</li>
</ul>
</template>

<script>
export default {
name: 'comments-list',
props: ['content']
}
</script>

It all looks very nice but there is one missing piece. What if I need to pass a blog post ID to BlogPost ? Or what if I need to emit an event from BlogPost to App component? With current implementation it won’t work.

Step 3. Handling Props and Events in Higher Order Component

Firstly, let’s change a bit the implementation of the getBlogPost method in DataSource . It needs to take the post’s id as an argument in order to know which post to fetch and return. Since the actual getBlogPost call happens inside BlogPost component it makes sense to pass as a prop the desired blog post id and make use of it when the getBlogPost method is called. In order to do so I need to do two things: pass theid prop from the App component down to the BlogPost component and change the function I pass to the Higher Order Component so it accepts the second argument — the props it has to pass down further to the BlogPost.

# App.vue
<template>
<div id="app">
<blog-post :id="1"/>
</div>
</template>

<script>
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => {
return DataSource.getBlogPost(props.id)
})
export default {
name: 'app',
components: {
'blog-post': BlogPostWithSubscription
}
}
</script>
---
# components/BlogPost.vue
<template>
<div>
{{content}}
</div>
</template>

<script>
export default {
props: ['content', 'id']
}
</script>

Now, I need to update the Higher Order Component so it knows how to pass down the props to the component it renders.

# hocs/withSubscription.js
import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component, selectData) => {
const originalProps = component.props || [];

return Vue.component('withSubscription', {
render(createElement) {
return createElement(component, {
props: {
...originalProps,
content: this.fetchedData
}
})
},
props: [...originalProps],
data() {
return {
fetchedData: null
}
},
methods: {
handleChange() {
this.fetchedData = selectData(DataSource, this.$props)
}
},
mounted() {
DataSource.addChangeListener(this.handleChange)
},
beforeDestroy() {
DataSource.removeChangeListener(this.handleChange)
}
})
}

export default withSubscription

First thing which is added to HOC is reading the original props from the component it renders. Those properties are saved inside originalProps constant. In Vue a component has to to define which props it accepts. The withSubscription has to accept the same props as the component it renders in order to be able to pass them down to it later. That’s done with this line of code:

return Vue.component('withSubscription', {
...
props: [...originalProps], # <= this line
...
}

The last piece which was updated is the selectData function call inside thehandleChange method. The second argument was added — the props of the Higher Order Component — this.$props. The $props property is a Vue Component instance property available since Vue version 2.2.

I covered passing down the props to a child component, the last piece missing is events emission from a child component up to its parent.

Let’s add event listener inside App.vue component and something that emits an event inside BlogPost.vue .

# App.vue

<template>
<div id="app">
<blog-post :id="1" @click="onClick"/>
</div>
</template>
---
# components/BlogPost.vue
<template>
<div>
<button @click="$emit('click', 'aloha')">CLICK ME!</button>
{{data}}
</div>
</template>
<script>
export default {
props: ['data', 'id']
}
</script>

It’s important to remember that I don’t render BlogPost directly inside App , there is a middleman — the withSubscription HOC.

In order to pass down event listeners to rendered component I need to add one line of code inside the Higher Order Component.

# hocs/withSubscription.js
return Vue.component('withSubscription', {
...
on: {...this.$listeners} # <= this line,
})


Similarly as with this.$props , there is an instance property $listener which contains the parent-scope v-on event listeners.


Thank you for reading this article. Comments are more than welcome. The main goal of writing this article is to share the knowledge but with each article I have also a second goal — to learn :)

The complete application from the article can be found on github: https://github.com/bognix/vue-hoc