Reusing Logic in Vue Components

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.

Component Inheritance

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  
};

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!

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.

Mixins

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.

Composing Reusable Components

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.

Conclusion

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.

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!


Article written by Alex Jover Morales

Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.