An important principle of software programming is D.R.Y (Don’t Repeat Yourself), which talks about reusing code and logic as much as possible and minimizing duplication. Component extensions avoid repetitive code and are easier to develop and maintain quickly. So what is the best way to extend Vue components?

Vue provides a number of apis and patterns to support component reuse and extension, which you can choose for your own purposes and preferences.

This article introduces some of the more common methods and patterns that I hope will help you.


Whether the extension component is necessary

Keep in mind that all component extension methods add complexity, extra code, and sometimes performance overhead.

Therefore, before deciding on extension components, it is a good idea to see if there are other, simpler design patterns that can accomplish the goal.

The following component design patterns are usually sufficient to replace extension components:

  • propsMatching template logic
  • Slot slot
  • JavaScript utility functions

propsMatching template logic

The easiest way to do this is to use props in conjunction with template conditional rendering.

For example, through the type property: myversatilecomponent.vue

<template>
  <div class="wrapper">
    <div v-if="type === 'a'">... </div> <div v-else-if="type === 'b'">... </div> <! --etc etc--> </div> </template> <script>export default {
  props: { type: String },
  ...
}
</script>

Copy the code

Passing different type values when using components can achieve different results.

// *ParentComponent.vue*
<template>
  <MyVersatileComponent type="a" />
  <MyVersatileComponent type="b" />
</template>

Copy the code

This pattern does not apply, or is not used correctly, if two things happen:

  1. The component composition pattern makes applications scalable by breaking state and logic into atomic parts. If there are a lot of conditional judgments in a component, readability and maintainability will deteriorate.
  2. The props and template logic is meant to make components dynamic, but there is also runtime resource consumption. If you use this mechanism to solve code composition problems at run time, that’s an anti-pattern.

Slot (s)

Another way to avoid component extensions is to take advantage of slots, where the parent component sets custom content within the child component.

// *MyVersatileComponent.vue*
<template>
  <div class="wrapper">
    <h3>Common markup</div>
    <slot />
  </div>
</template>

Copy the code
// *ParentComponent.vue*
<template>
  <MyVersatileComponent>
    <h4>Inserting into the slot</h4>
  </MyVersatileComponent>
</template>

Copy the code

Render result:

<div class="wrapper">
  <h3>Common markup</div>
  <h4>Inserting into the slot</h4>
</div>

Copy the code

One potential constraint of this pattern is that the elements in the slot are subordinate to the context of the parent component, which may not be natural when splitting logic and state. Scoped Slot is more flexible and will be covered later in the Non-rendering Components section.

JavaScript utility functions

If you only need to reuse individual functions between components, then you can simply extract these JavaScript modules without using the component extension pattern at all.

JavaScript’s module system is a very flexible and robust way to share code, so you should rely on it as much as possible. MyUtilityFunction.js

export default function() {... }Copy the code

MyComponent.vue

import MyUtilityFunction from "./MyUtilityFunction";
export default {
  methods: {
    MyUtilityFunction
  }
}

Copy the code

Several patterns for extending components

If you have considered the above simple models, they are not flexible enough to meet your needs. Then you can consider extending the component.

The four most popular ways to extend Vue components are:

  • The Composition function
  • mixin
  • High order Component (HOC)
  • No rendering component

Each approach has its own strengths and weaknesses, and more or less each has its own parts, depending on the use scenario.

The Composition function

The latest approach to sharing state and logic between components is the Composition API. This is an API from Vue 3 and can also be used as a plug-in in Vue 2.

Instead of declaring properties such as Data, computed, and methods in component definition configuration objects, the Composition API declares and returns these configurations through a setup function.

For example, declare the Counter component with Vue 2 configuration properties like this: counter.vue

<template>
  <button @click="increment">
    Count is: {{ count }}, double is: {{ double }}
  </button>
<template>
<script>
export default {
  data: () => ({
    count: 0
  }),
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    double () {
      return this.count * 2;
    }
  }
}
</script>

Copy the code

Refactor this component using the Composition API, which does exactly the same thing: counter.vue

<template><! --as above--><template> <script> import { reactive, computed } from"vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    });

    function increment() {
      state.count++
    }

    return {
      count,
      double,
      increment
    }
  }
}
</script>

Copy the code

One of the main benefits of declaring components using the Composition API is that it makes logical reuse and extraction very easy.

Further refactoring moved the counter functionality to the JavaScript module usecounter.js: usecounter.js

import { reactive, computed } from "vue";

export default function {
  const state = reactive({
    count: 0,
    double: computed(() => state.count * 2)
  });

  function increment() {
    state.count++
  }

  return {
    count,
    double,
    increment
  }
}

Copy the code

Counter functionality can now be seamlessly incorporated into any Vue component through the setup function: myComponent.vue

<template><! --as above--></template> <script> import useCounter from"./useCounter";

export default {
  setup() {
    const { count, double, increment } = useCounter();
    return {
      count,
      double,
      increment
    }
  }
}
</script>

Copy the code

Composition functions make functionality modular and reusable, and are the most straightforward and cost-effective way to extend components.

Disadvantages of the Composition API

The downsides to the Composition API aren’t really that much — it may just seem a bit wordy, and the new usage is a bit foreign to some Vue developers.

When To Use The New Vue Composition API (And When Not To)

mixin

If you’re still using Vue 2, or just prefer to define component functionality in the way you configure objects, use mixin mode. Mixins extract common logic and state into separate objects that are merged with the internally defined objects of the components that use mixins.

Continuing with the Counter component example, we put the common logic and state into the Countermixin.js module. CounterMixin.js

export default {
  data: () => ({
    count: 0
  }),
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    double () {
      returnthis.count * 2; }}}Copy the code

Using mixins is also as simple as importing the corresponding module and adding variables to the mixins array. Component initialization merges the mixin object with the component’s internal definition object. MyComponent.vue

import CounterMixin from "./CounterMixin";

export default {
  mixins: [CounterMixin],
  methods: {
    decrement() { this.count--; }}}Copy the code

Option to merge

What if an option in a component conflicts with a mixin?

For example, defining a component’s own increment method, which has a higher priority? MyComponent.vue

import CounterMixin from "./CounterMixin";

exportDefault {mixins: [CounterMixin], methods: {// Increment method overwrite mixin's increment method.increment() {... }}}Copy the code

This is where Vue’s merge strategy comes in. Vue has a set of rules that determine how options with the same name are handled.

Often, the options from the mixin are overridden by the component’s own options. There are exceptions, such as the same type of lifecycle hooks that are not overwritten directly, but are placed in arrays and executed sequentially.

You can also change the default behavior by customizing merge policies.

The disadvantage of a mixin

As a pattern for extending components, mixins work fine for simple scenarios, but once they scale up, they become problematic. Not only do you need to be aware of naming conflicts (especially with third-party mixins), but it can be difficult to figure out where a feature is coming from when components of multiple mixins are used, and to locate problems.

High order component

HOC is a concept borrowed from React and can also be used by Vue.

To understand this concept, let’s get rid of components and look at two simple JavaScript functions, increment and double.

function increment(x) {
  return x++;
}

function double(x) {
  return x * 2;
}

Copy the code

Suppose we want to add a function to both of these functions: output results at the console.

To do this, we can use higher-order function mode and create a new addLogging function that takes a function as an argument and returns a function with new functionality.

function addLogging(fn) {
  return function(x) {
    const result = fn(x);
    console.log("The result is: ", result);
    return result;
  };
}

const incrementWithLogging = addLogging(increment);
const doubleWithLogging = addLogging(double);

Copy the code

How does a component take advantage of this pattern? Similarly, we created a higher-order component to render the Counter component and added a Decrement method as an instance property.

The actual code is complicated, and only the pseudo-code is given here as an illustration:

import Counter from "./Counter"; // Const CounterWithDecrement => ({render(createElement) {const options = {decrement() { this.count--; }}returncreateElement(Counter, options); }});Copy the code

HOC mode is cleaner and more scalable than mixin, but at the cost of adding a wrapped component that is tricky to implement.

No rendering component

If you need to use the same logic and state on multiple components, but present them differently, consider the no-render component pattern.

This pattern requires two types of components: a logical component for declaring logic and state, and a presentation component for presenting data.

Logical component

Going back to the Counter example, suppose we need to reuse this component in multiple places, but in different ways.

Create a CounterRenderless. Js to define logical components that contain logic and state, but do not contain templates. Instead, declare scoped slots through the render function.

Scoped Slot exposes three properties for the parent component to use: state count, method increment, and calculation property double. CounterRenderless.js

export default {
  data: () => ({
    count: 0
  }),
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    double () {
      returnthis.count * 2; }},render() {
    return this.$scopedSlots.default({
      count: this.count,
      double: this.double,
      increment: this.toggleState,
    })
  }
}

Copy the code

The scoped slot here is the key to the logical components in this pattern.

Display components

Next comes the presentation component, which provides concrete presentation as a user of non-rendered components.

All element tags are contained in scoped slot. As you can see, these properties are used just as templates are placed directly in logical components. CounterWithButton.vue

<template>
  <counter-renderless slot-scope="{ count, double, increment }">
    <div>Count is: {{ count }}</div> 
    <div>Double is: {{ double }}</div>
    <button @click="increment">Increment</button>
  </counter-renderless>
</template>
<script>
import CounterRenderless from "./CountRenderless";
export default {
  components: {
    CounterRenderless
  }
}
</script>

Copy the code

The non-rendering component pattern is very flexible and easy to understand. However, it is less generic than the previous approaches and may have only one application, which is for developing component libraries.

Template extensions

One limitation of both the API and the design pattern is the inability to extend a component’s template. Vue has a way to reuse logic and state, but not template tags.

One way to hack is to use HTML preprocessors, such as Pug, to handle template extensions.

The first step is to create a base template *.pug* file containing common page elements. Also include a block input that acts as a placeholder for the template extension.

BaseTemplate.pug

div.wrapper h3 {{ myCommonProp }} <! --common markup--> block input <! --extended markup outlet -->Copy the code

To extend this template, you need to install the Pug plug-in for the Vue Loader. You can then introduce the base template and replace the placeholder with block input syntax: myComponent.vue

<template lang="pug"> extends BaseTemplate.pug block input h4 {{ myLocalProp }} <! --gets includedin the base template-->
</template>

Copy the code

At first you might think that this is the same concept as slot, but the difference is that the base template does not belong to any single component. It is merged with the current component at compile time, rather than replaced at run time like slot.

References:

  • Mixins Considered Harmful – Dan Abramov
  • Renderless Components in Vue.js – Adam Wathan

See this rather energetic logo, don’t pay attention to it?