Do we need Higher Order Components in Vue.js?
In my previous article I described a way how to create Higher Order Components in Vue.js. I received a lot of great feedback which inspired me to write this article.
Before starting looking for an answer for the question: Do we need Higher Order Components in Vue.js? I would like to ask another question: What problems Higher Order Components solve? I believe the answer for that question is crucial.
For me there are two main problems HOCs solve:
- code repetition: they allow to share common functionality between different components,
- code organization: they allow to extend a component with a functionality on the instantiation level, not on the declaration level
Vue.js provides us with two solutions to address the problem of code repetition and to improve the code organization: mixins and scoped slots. In this article I’m going to compare those two with the Higher Order Component approach I described in the previous article.
Approach 0. Higher Order Component
This post is based on the example from the Higher Order Components in Vue.js article. Below is the code of the application from the previous article for the reference.
First, the main Application component:
Second, the CommentsList
and the BlogPost
components:
And finally, the withSubscription
Higher Order Component:
withSubscription
Higher Order Component has four responsibilities:
- fetch the data from external data source
- pass fetched data to the wrapped component
- add the change listener to the external data source when component is mounted
- remove the change listener from the external data source when component is destroyed
Approach 1: Mixin
The mixin which has the same responsibilities as the withSubscription
Higher Order Component looks a lot simpler than HOC
Inside handleChange
method the selectData
method is called, however the mixin is missing it’s implementation. In HOC approach the selectData
was passed as an argument to withSubscription
Higher Order Component because it’s implementation differs in BlogPost
and CommentsList
. When using mixin I have to implement this method inside both componens.
Below are the BlogPost
and CommentsList
components with the mixin attached:
App
component looks a lot simpler, instead of wrapping components with a HOC I just use BlogPost
and CommentsList
components.
Above mixin implementation solves one of two problems the HOC solved. It allows to extract the shared functionality and avoid the code repetition between components. However, it doesn’t address the second problem. With mixin I can’t create a BlogPost
or CommentsList
without a functionality from the mixin. I can always pass a prop to disable mixin’s functionality, however what if I need mixin’s logic only in one BlogPost
instance and I don’t want it in remaining 100 instances? I don’t want to have a logic in my component that I don’t use in most cases.
What is more, I find it hard to debug when the method implementation is inside different file from the file inside which it’s called. With mixin approach I defined selectData
inside components but I called it inside the mixin. For me it contradicts the idea of Single File Components, the idea that makes Vue so cool. With HOC I also defined selectData
in different file from which it was called, however I was using props to pass this method so at least I knew from where it’s coming from.
Approach 2. Mixin++
The main disadvatage of Approach 1 is that a component and a mixin are always together. I can’t create two instances of a component, one with, and one without a mixin.
Well, that isn’t completely true, in Vue I can attach a mixin to a component “on demand”, they don’t have to be bound together in all cases. Below is the example of how to do so.
Firstly, the BlogPost
and CommentsList
components have to be changed. I no longer want to include mixin inside them.
Mixin is gone but the selectData
method has to stay. The same rules apply as in the first Approach — CommentsList#selectData
method’s definition differs from BlogPost
's one.
In order to use withSubscription
mixin on demand I had to change App
component.
Whenever I want to use a component with a mixin I call Vue.extend(%mixin%).extend(%component%)
inside component’s definition.
This approach looks really promising. It solves the same problems as HOCs, I can share logic between components and I can “attach” this logic to a component only when I need to use it. Unfortunately it is not perfect. First of all, I had to declare selectData
method inside BlogPost
and CommentsList
components even though I may not need it in most cases I can imagine that after some time I may forget that selectData
method is called by a mixin and as a result I may remove it and then land in a debugging hell. What is more, the withSubscription
mixin provides data to a component — fetchedData
. I have to add a fallback value inside component’s data in case I want to use a component without a mixin. Without fallback I’ll get an error in console saying that I’m referencing property that is not defined on the instance.
export default {
data() {
return {
fetchedData: ''
}
},
methods: {
handleChange() {
this.fetchedData = this.selectData(...)
}
},
....
}
Approach 3. Scoped Slots
At first, it was very hard for me to get my head around scoped slots. The official documentation wasn’t very helpful. Before I start explaining how to convert HOC into Scoped Slot I’m going to shortly explain what’s the idea behind Scoped Slots.
Scoped Slots give possibility to pass properties to a component that is rendered inside the slot of different component. The way we define scoped slots is not very different from standard slots definition. Let’s imagine we have a ComponentParent
that contains scoped slot inside it. What is more this component contains a computed property called someComputed
. The scoped slot is defined as follows:
<slot :data="someComputed"></slot>
It looks very much alike with regular props passing between components. The tricky part is the usage of scoped slot. Let’s imagine we have another component, a ComponentChild
. What we want to achieve is to render ComponentChild
inside ComponentParent
slot and pass the someComputed
value from ComponentParent
to ComponentChild
.
The usage of scoped slot looks as follows
<component-parent>
<component-child :slot-scope="parentScope" :parentData="parentScope.data"/>
</component-parent>
and here’s what happens inside the example above:
- the
ComponentChild
is rendered inside the slot declared inComponentParent
- the
ComponentChild
shares the scope ofComponentParent
and stores the reference to this scope insideparentScope
property - the
ComponentChild
reads thedata
property fromparentScope
. The value ofparentScope.data
is the value of computed propertysomeComputed
fromComponentParent
With this very simple scoped slots definition in place I can move on to describing how to replace the Higher Order Component with Scoped Slot.
Firstly, the CommentsList
and BlogPost
components — they look exactly the same as in the approach with Higher Order Component, which is great. There is no need to add any extra code inside components definitions in order to use scoped slot.
Secondly, the definition of the component with Scoped Slot, let’s call the component: WithSubscription
:
The component contains all the shared functionalities:
- it attaches change listeners when component is created and detaches them when component is destroyed
- it updates the data on every change inside the
DataSource
- it calls
selectData
method which is passed as a prop and can be different in each usecase - it passes the
fetchedData
down to a component it renders
And finally the usage:
The code above is only slightly more complex than the one from the example from the beginning of this paragraph.
The BlogPost
and CommentsList
components are rendered inside the scoped slot. The WithSubscription
component accepts as a prop the selectData
method which is different for each component. Then, each component has the access to fetchedData
from WithSubscription
component via the scope called withSubscriptionScope
.
Summary
The Scoped Slots solve the same problems as Higher Order Components. They allow to share common functionality and they allow to add this functionality to a component only in some cases.
Are they better than Higher Order Components? It’s hard to say. They are a different approach to solve the same problem.
Are they better than Higher Order Components inside Vue.js application? Probably yes. Scoped Slots solution is provided by Vue.js, HOCs on the other hand are custom implementation that may break when Vue.js internals change.
Scoped Slots solve one more problem. They prevent naming collisions. Developer has to declare the name of the scope, hence it’s easy to follow from where each prop is taken.
If you would like to learn more about the naming collision in HOCs I recommend Michael Jackson’s video: Use a Render Prop! and Chang Wang’s article: Solving the problems of Higher Order Components without throwing the baby out with the bathwater
You can find the code from this article in github repo. You can find there 4 branches, one for each approach described in the article.