One day you start creating a Vue application. You start creating components in order to structure the different pieces of your application. That's cool and you should be feeling the great dev experience of Vue and the web component architecture. As you go on with your project you start to somehow structure your application components, probably by pages and components.
But as the project continues to grow, you start identifying logic that it's repeated across several components. We've been told about Don't Repeat Yourself (DRY) and Keep It Simple, Stupid (KISS), two principles that allow us to write software that is easy to use and to maintain.
Maybe you already know some patterns, libraries and techniques that help following those principles. Vuex will help you to pull out the state logic from the components, Vue router will do the same for the routing logic... but what about the components?
Often we come across the case where we need to reuse some UI functionality that belongs to the components. For instance, both a Popover and a Tooltip can share the functionality of opening and closing when a certain event triggers, in addition to being anchored and positioned to an element.
In this article, we're going to take the example of a Colorful.vue component that listens for the scroll event and changes the color depending on the scroll position: if it's higher than the window height it'll be blue, otherwise red. That's achieved by binding a color
local state variable using :style
:
<!-- Colorful.vue -->
<template>
<div :style="{ background: color }">
</div>
</template>
<script>
export default {
data: () => ({
color: "red"
}),
methods: {
handleScrollChange(status) {
this.color = status === "in" ? "red" : "blue";
},
handleScroll() {
if (window.scrollY > window.innerHeight) {
this.handleScrollChange("out");
} else {
this.handleScrollChange("in");
}
}
},
mounted() {
window.addEventListener("scroll", this.handleScroll);
},
destroyed() {
window.removeEventListener("scroll", this.handleScroll);
}
};
</script>
You might not see anything wrong in the component. It registers the scroll
event in the mounted
hook and un-registers it in the destroyed
. They call a handleScroll
methods that perform the scroll position check and calls the handleScrollChange
method to change the color, depending on the status
argument.
Right now the scrolling functionality is inside the Colorful
component. However, we could take out the mounted
and destroyed
hooks and the handleScroll
methods in order to reuse them in other components.
Let's see different ways to do that.
First let's start by moving the scroll-related behaviour into its own component Scroll.js:
export default {
methods: {
handleScroll() {
if (window.scrollY > window.innerHeight) {
this.handleScrollChange("out");
} else {
this.handleScrollChange("in");
}
}
},
mounted() {
window.addEventListener("scroll", this.handleScroll);
},
destroyed() {
window.removeEventListener("scroll", this.handleScroll);
}
};
As you can see, this Scroll component expects a handleScrollChange
to exist, which we implement in the child component, meaning that this component must be extended and cannot work on its own.
Since it only includes JavaScript, we can write it in a .js file, but it could be a .vue file as well if needed. Just keep in mind that only the JavaScript part of a .vue file will be inherited.
Then, in the Colorful component, let's remove all the component behaviour that we moved out and import the Scroll file using the extends
component option:
<!-- Colorful.vue -->
<template>
<div :style="{ background: color }"></div>
</template>
<script>
import Scroll from "./scroll";
export default {
extends: Scroll,
data: () => ({
color: "red"
}),
methods: {
handleScrollChange(status) {
this.color = status === "in" ? "red" : "blue";
}
}
};
</script>
Be aware that component extension is not as classes inheritance. In this case, Vue merges both the parent and child component options creating a new mixed object.
For instance, in the previous example we’ll end up having a component with the following API:
{
data: () => ({
color: "red"
}),
methods: {
handleScrollChange(status),
handleScroll,
},
mounted,
destroyed
};
For the case of the mounted
and destroyed
hook, both the parent’s and children’s are kept and they will be called in inheritance order, from parent to children.
Similar to component inheritance, we can use mixins in order to share component logic. But while in the previous example we can only inherit from one component, with mixins we can combine the functionality of multiple of them.
In fact, we don't need to change anything from the Scroll.js file from the previous example. Just by using the mixins
options instead of extends
on the Colorful component would be enough. Keep in mind that mixins
expect an array:
<!-- Colorful.vue -->
<script>
import Scroll from "./scroll";
export default {
mixins: [Scroll],
data: () => ({
color: "red"
}),
methods: {
handleScrollChange(status) {
this.color = status === "in" ? "red" : "blue";
}
}
};
</script>
The main difference with component inheritance is that the order is different: while in component inheritance the child's hooks are executed before the parent's, with mixins their hooks are executed before the component's using it.
Additionally, mixins cannot have any template or style tags, they're just plain JavaScript.
While inheritance and mixins seem easy to implement, they're somehow implicit. In the previous example, when you use Scroll you need to know that you have to implement the handleScrollChange
method.
In this case is not that bad, but when you're extending or using multiple mixins, things can start to become confusing and you begin to trace functionality across many pieces.
Another way to reuse functionality is by creating reusable components that just receive props and emit events, which makes it a more explicit solution that neither has any magic merging nor shares the context. Additionally, this solution is more "universal" since the same approach can be applied in any other component-based technology.
First, we have to make Scroll a .vue component:
<!-- Scroll.vue -->
<template>
<div></div>
</template>
<script>
export default {
methods: {
handleScroll() {
if (window.scrollY > window.innerHeight) {
this.$emit("scrollChange", "out");
} else {
this.$emit("scrollChange", "in");
}
}
},
mounted() {
window.addEventListener("scroll", this.handleScroll);
},
destroyed() {
window.removeEventListener("scroll", this.handleScroll);
}
};
</script>
In this case, instead of calling a magic method handleScrollChange
we're emitting a scrollChange
event that the parent component can use to do its thing.
Then, in Colorful.vue we must import it as a component and handle the scrollChange
event:
<!-- Colorful.vue -->
<template>
<scroll
@scrollChange="handleScrollChange"
:style="{ background: color }">
</scroll>
</template>
<script>
import Scroll from "./scroll";
export default {
components: {
Scroll
},
data: () => ({
color: "red"
}),
methods: {
handleScrollChange(status) {
this.color = status === "in" ? "red" : "blue";
}
}
};
</script>
We've replaced the div
tag by scroll
, but the Colorful component logic remain the same.
We've seen three ways to take out the scroll functionality and reuse it. Depending on your case you'll use one or another.
Component inheritance and mixins give us a way to magically separate part of the logic of a component and merge it together, which can be suitable for some cases as long as it doesn't become too confusing. Mixins, in particular, are more powerful since they allow to combine multiple of them.
Using component composition is a cleaner and more explicit solution. The same technique could be used in any other component-based framework. In some cases though, it might not be as convenient as mixins.
You can see the demo in this Codesandbox where you can change the Example
in the index.js file to try the three of them.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.