Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

How to wrap, extend, or proxy a Vue component

12 min read 3401 105

Introduction

As an experienced developer using the Vue.js framework for frontend app development, you’ll most often find the need to apply design patterns to your codebase in order to build reusable, readable, and scalable components, extend existing components, or even control their behavior to meet specific requirements.

This process may involve wrapping components, extending them, or even creating proxies around them. Whether you’re aiming to enhance a component’s functionality, embed it within another component, or intervene in its communication with the rest of your application, understanding these advanced techniques is crucial.

In this article, I will go in-depth on these concepts, briefly explaining why you need them, showing how they work in JavaScript, how to use them with Vue.js components, and alternative methods.

Jump ahead:

How to extend Vue components

The extends keyword is used in class declarations or class expressions to create a class that is a child of another class:

class ChildClass extends ParentClass { /* … */ }

Why you should extend your Vue components

Extending Vue components offers reusability, code organization, flexibility, customization, and maintainability, and promotes effective team collaboration. By inheriting properties, methods, and templates from a base component, you can create modular and consistent code structures while adding or overriding specific features.

This approach allows you to customize components without modifying the original code, improving maintainability and promoting code reuse. Furthermore, it facilitates teamwork by enabling different team members to work on different components simultaneously without conflicts, fostering a scalable and efficient development process.

Methods for extending Vue components

There are two primary methods to extend Vue components and add custom functionality to them:

  1. Mixins
  2. Composition API

Mixins

If you are working with Vue 2 or prefer an options-based approach to organizing component features, you can use the mixin pattern. This pattern involves extracting shared logic and state into a separate object, which can then be merged with the definition object of a component that uses it.

To create a mixin, extract the shared logic and state into a JavaScript module:

//GreetingMixin.js
export const greetingMixin = {
  computed: {
    greeting() {
      return 'Hello there!';
    }
  },
  created() {
    console.log('Mixin created');
  }
};

You can use the mixin by importing the greetingMixin module and adding it to the mixins array in its definition. The mixin object will merge with the definition when this component is instantiated:

<template>
  <div>
    <h1>{{ greeting }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

<script>

export default {
  mixins: [greetingMixin],
  data() {
    return {
      description: 'Welcome to my Vue app.'
    };
  },
  created() {
    console.log('Component created');
  }
};
</script>

Mixins can be effective for extending components in simple cases, but they may pose challenges as your project scales. Naming collisions can become a concern, particularly when incorporating third-party mixins. Understanding the overall behavior of a component and managing complexity can become more challenging when relying heavily on mixins.

Composition API

With Vue 3, you can use the Composition API to create composable functions that encapsulate reusable logic. You can import these functions and use them in multiple components, promoting code reuse and encapsulation.

Similarly, you extract the shared logic and state into a separate JavaScript module:

/useCounter.js
import { ref, computed } from 'vue';

export default function() {

    const counter = ref(0);

    const incrementCounter = () => {
      counter.value++;
    };

    const greeting = computed(() => {
      return `Hello there! Counter value: ${counter.value}`;
    });

    return {
      counter,
      incrementCounter,
      greeting
    };
}

Now the extracted shared logic can be seamlessly introduced into any Vue component using the setup function:

<template>
  <div>
    <h1>{{ greeting }}</h1>
    <p>{{ description }}</p>
    <button @click="incrementCounter">Increment Counter</button>
  </div>
</template>

<script>
import useCounter from "./components/useCounter";

export default {
  setup() {
    const { counter,incrementCounter,greeting } = useCounter();
    return {
      counter,incrementCounter,greeting
    }
  },
  data() {
    return {
      description: 'Welcome to my Vue app.'
    };
  },
  created() {
    console.log('Component created');
  }
};
</script>

The Composition API also offers better compatibility with TypeScript and access to lower-level features like reactivity, dependency injection, and lifecycle hooks.

Simpler alternatives to extending Vue components

There are simpler alternatives to extending Vue components that can achieve similar results without the complexity of component inheritance. Here are a few alternatives:

  1. Props
  2. Slots
  3. Utility functions

Let’s go into each of them.

Props

Rather than extending a component to modify its behavior, you can pass props to the component to customize its functionality. By providing different configurations or props, you can achieve the desired behavior without altering the component itself:

//MyComponent.vue
<template>
  <div>
    <h1>{{ greeting }}</h1>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      greeting: 'Hello!'
    };
  }
};
</script>

When using this component, pass a value to the message prop:

<template>
  <div>
    <my-component message="Welcome to my Vue app!" />
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue';

export default {
  components: {
    MyComponent
  }
};
</script>

Slots

Vue’s slot feature allows you to pass content and template fragments from a parent component to a child component, and allow the child component to render the fragment within its own template.



When you use slots effectively, you can create flexible and customizable components that can be used in various contexts without the need for inheritance or extensive modifications:

//LikeButton.vue
<template>
  <button class="btn">
    <slot></slot>
  </button>
</template>

<script>
export default {
};
</script>

<style>
.btn {
  display: flex;
  align-items: center;
  border: none;
  padding: 4px 18px;
}
</style>

The <slot> element serves as a slot outlet, specifying where the content provided by the parent component should be rendered. The child component can define one or more <slot> elements to indicate where the dynamic content should be inserted, providing a flexible and customizable way to compose components:

<template>
  <div>
    <h1>LinkedIn Like button</h1>
    <like-button>
      <svg xmlns="<http://www.w3.org/2000/svg>" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - <https://fontawesome.com> License - <https://fontawesome.com/license> (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="gray" d="M313.4 32.9c26 5.2 42.9 30.5 37.7 56.5l-2.3 11.4c-5.3 26.7-15.1 52.1-28.8 75.2H464c26.5 0 48 21.5 48 48c0 18.5-10.5 34.6-25.9 42.6C497 275.4 504 288.9 504 304c0 23.4-16.8 42.9-38.9 47.1c4.4 7.3 6.9 15.8 6.9 24.9c0 21.3-13.9 39.4-33.1 45.6c.7 3.3 1.1 6.8 1.1 10.4c0 26.5-21.5 48-48 48H294.5c-19 0-37.5-5.6-53.3-16.1l-38.5-25.7C176 420.4 160 390.4 160 358.3V320 272 247.1c0-29.2 13.3-56.7 36-75l7.4-5.9c26.5-21.2 44.6-51 51.2-84.2l2.3-11.4c5.2-26 30.5-42.9 56.5-37.7zM32 192H96c17.7 0 32 14.3 32 32V448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V224c0-17.7 14.3-32 32-32z"/></svg>
      <span style="color:gray; margin-left: 4px ">Like</span>
    </like-button>
  </div>
</template>

<script>
import LikeButton from './components/LikeButton.vue';

export default {
  components: {
    LikeButton
  }
};
</script>

With slots, expressions in the parent template only have access to the parent scope; expressions in the child template only have access to the child scope.

Utility functions

You can also create reusable functions with well-defined logic and compose them together to build more complex structures, promote code reuse and modularity, and improve separation of concerns:

// Utility function
export const capitalize = (str) => {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
};

Now the utility function can be seamlessly introduced into any Vue component using the import syntax:

<template>
  <div>
    <h1>{{ formattedMessage }}</h1>
  </div>
</template>

<script>
import { capitalize } from "./utility"

export default {
  data() {
    return {
      message: 'hello world'
    };
  },
  computed: {
    formattedMessage() {
      return capitalize(this.message);
    }
  }
};
</script>

How to wrap Vue components

In JavaScript, a wrapper refers to a function that is designed to invoke one or multiple other functions, often for the sake of convenience or to adapt them to perform slightly different tasks.

Wrappers provide a layer of abstraction, allowing for additional functionality or customization to be added around the original functions:

function multiplyByTwo(number) {
  return number * 2;
}

function addOne(number) {
  return number + 1;
}

function wrapperFunction(func, number) {
  const result = func(number);
  return `Wrapper: The result is ${result}`;
}

console.log(wrapperFunction(multiplyByTwo, 5)); // Output: Wrapper: The result is 10
console.log(wrapperFunction(addOne, 3)); //Output: Wrapper: The result is 4

Why you should use wrapper components in Vue

Wrapper components in Vue provide encapsulation and reusability, facilitate code composition and organization, offer customization and flexibility. They promote code reusability and modularity by encapsulating related components or logic into a single component, and allow composition of smaller components for better code organization.

They also provide customization options and flexibility, enabling components to be reused in different contexts, which aids in extending native/third party elements.

Customizing third-party libraries with the wrapping technique

When using third-party libraries, you might find that a component almost meets your needs but requires a few changes. Rather than editing the component directly, which may be overwritten when you update the package, you can wrap it and add your modifications to the wrapper.

Let’s customize a third-party SweetAlert component using the wrapping technique:

//SweetAlertWrapper.vue
<template>
  <button @click="showAlert">
    <slot />
  </button>
</template>

<script>
import Swal from "sweetalert2";

export default {
  methods: {
    showAlert() {
      Swal.fire({
        title: "Custom Alert",
        text: "This is a customized alert!",
        icon: "success",
        showCancelButton: true,
        confirmButtonText: "Custom Button",
        cancelButtonText: "Cancel",
      }).then((result) => {
        if (result.isConfirmed) {
          this.customAction();
        } else if (result.dismiss === Swal.DismissReason.cancel) {
          this.cancelAction();
        }
      });
    },
    customAction() {
      // Add your custom action logic here
      console.log("Custom action performed!");
    },
    cancelAction() {
      // Add your cancel action logic here
      console.log("Cancel action performed!");
    },
  },
};
</script>

<style scoped>
button {
  margin-top: 10px;
}
</style>

Here’s our wrapper component that displays a button. When the button is clicked, it triggers a customized alert dialog using SweetAlert2 library. The dialog allows the user to confirm or cancel and based on their choice, specific actions are performed by calling the appropriate methods in the Vue component.

The wrapper component can now be easily integrated into any Vue component as follows:

//App.vue
<template>
  <div>
    <SweetAlertWrapper> Show Alert </SweetAlertWrapper>
  </div>
</template>

<script>
import SweetAlertWrapper from "./components/SweetAlertWrapper.vue";
export default {
  components: { SweetAlertWrapper },
};
</script>

Here’s the SweetAlertWrapper component refactored to use the Vue 3 Composition API. Note that the functionality is unchanged:

//SweetAlertWrapper.vue
<template>
  <button @click="showAlert">
                <slot/>
        </button>
</template>

<script>
import { ref } from 'vue';
import Swal from 'sweetalert2';

export default {
  setup() {
    const showAlert = () => {
      Swal.fire({
        title: 'Custom Alert',
        text: 'This is a customized alert!',
        icon: 'success',
        showCancelButton: true,
        confirmButtonText: 'Custom Button',
        cancelButtonText: 'Cancel',
      }).then((result) => {
        if (result.isConfirmed) {
          customAction();
        } else if (result.dismiss === Swal.DismissReason.cancel) {
          cancelAction();
        }
      });
    };

    const customAction = () => {
      // Add your custom action logic here
      console.log('Custom action performed!');
    };

    const cancelAction = () => {
      // Add your cancel action logic here
      console.log('Cancel action performed!');
    };

    return {
      showAlert,
      customAction,
      cancelAction,
    };
  },
};
</script>

<style scoped>
button {
  margin-top: 10px;
}
</style>

In this version, we use the Composition API with the setup function in SweetAlertWrapper.vue. The ref function is used to create reactive references for the functions showAlert, customAction, and cancelAction.


More great articles from LogRocket:


How to proxy Vue components

Before we get into proxying Vue components, let’s go over the definition of JavaScript Proxies.
JavaScript Proxies serve as intermediaries between objects and their interactions, allowing developers to intercept and modify actions such as property access, assignment, and function calls.

Proxies are an effective tool for improving application development because they provide benefits such as validation, encapsulation, and code maintainability.

Per MDN, in JavaScript, a proxy is defined with two parameters:

  • target: the original object which you want to proxy
  • handler: an object that defines which operations to intercept and how to redefine intercepted operations

The following basic syntax can be used to create a JavaScript Proxy:

const target = {
  firstname: "Paul",
  lastname: "Walker",
};

const handler = {
  get(target, prop, receiver) {
    return "John";
  },
};

const JSProxy = new Proxy(target, handler);
console.log(JSProxy.firstname); // Result: "John"
console.log(JSProxy.lastname); // Result: "John"

This code snippet creates a JavaScript Proxy named JSProxy using the Proxy constructor. The target object is wrapped by the proxy, and the handler object specifies the custom behavior for the proxy.

Now, if you access any property on JSProxy, it will always return the string “John” due to the custom behavior defined in the get trap of the handler object.

What are Proxy UI components?

Proxy UI components are wrapper components that encapsulate and customize external UI library components or your own UI components. Instead of directly using and referencing the external UI library components, you create wrapper components with your own product-specific prefix.

If you are using an external UI library and want to customize its components, you can create wrapper components that serve as an interface to the library components. This allows you to customize and control the behavior and appearance of the library components according to your needs.

This is also useful in cases where the external UI library doesn’t have a specific widget you need, or the required customization is complex — you can create your own UI components as wrapper components.

Why you should use proxy UI components

Proxy UI components provide a centralized entry point for both custom and external components. This centralization allows you to conveniently remove, modify, and add components based on your current needs and the stage of your product.

You can quickly build your application and prioritize feature development by using external UI library components at first. However, as your project evolves and new requirements emerge, you may run into limitations with the external components. Migrating to custom components wrapped in proxy UI components becomes an option at this point. These proxy components serve as intermediaries, allowing you to transition to more flexible and customizable solutions gradually.

How to proxy Vue components using vue-proxi

The vue-proxi library is a lightweight proxy component that acts as an intermediary between the parent and target components. It leverages Vue’s Template API and doesn’t reinvent component communication mechanisms.

Here are the key features of vue-proxi:

  • Uses Vue’s Template API: It takes advantage of Vue’s existing Template API, allowing for seamless integration into Vue projects
  • Powerful provide/inject: It enhances the familiar concept of provide/inject by providing additional capabilities and functionalities
  • Reactive: All data injected through <proxi> is reactive, ensuring that any changes to the injected data are automatically reflected in the component
  • Lightweight: With a small size of just 755 bytes when gzipped, it offers an efficient and lightweight solution for component communication

Using the <proxi> component, you can simplify and enhance component communication within your Vue application without adding unnecessary complexity or overhead.

Install vue-proxi with the following command:

npm i vue-proxi

Parent component

Import and register:

import Proxi from 'vue-proxi'

Insert the following anywhere in your template:

<proxi :proxi-key="key" [... attr / :prop / @listener]>

key is used to communicate with the child. Use a unique string value or a Symbol:

//Parent.vue
<template>
  <div>
    <proxi :proxi-key="key" :count="count" @increment="increment">
      <slot />
    </proxi>
    <div class="parent-info">
      <p>Count from parent component: {{ count }}</p>
      <button @click="increment">Increment Count (Parent)</button>
    </div>
  </div>
</template>

<script>
import Proxi from "vue-proxi";

export default {
  components: {
    Proxi,
  },

  data() {
    return {
      key: "proxi-demo",
      count: 0,
    };
  },

  methods: {
    increment() {
      this.count++;
    },
  },
};
</script>

In this example, we have a parent component (Parent) that wraps a single instance of a child component (Child) using the <proxi> component from the vue-proxi library. The <proxi> component provides and injects the count data between the parent and child components.

The parent component maintains a count variable, initially set to 0, which provides the count value and an increment method to the child component using the <proxi> component.

Child component

Import the Proxi Inject mixin:

import { ProxiInject } from 'vue-proxi'

Register the mixin:

//Child.vue
<template>
  <div class="child-component">
    <h3>Child Component</h3>
    <p>Count from parent: {{ count }}</p>
    <button @click="increment">Increment Count (Child)</button>
  </div>
</template>

<script>
import { ProxiInject } from "vue-proxi";

export default {
  mixins: [
    ProxiInject({
      from: "proxi-demo",
      props: ["count"] // Becomes available on VM as `this.count`,
    }),
  ],

  methods: {
    increment() {
      this.$emit("increment");
    },
  },
};
</script>

The child component receives the count value through the <proxi> component using the ProxiInject mixin from the vue-proxi library. It displays the count value and has a button to increment the count.

When the button is clicked, the child component emits the increment event, which is then caught by the parent component to update the count.

The injected data is all available in this.$$:

  • this.$$.class: Class
  • this.$$.props: Props (Automatically bound to VM)
  • this.$$.attrs: Attributes
    • e.g., v-bind="$$.attrs" or v-bind="{ ...$attrs, ...$$.attrs }"
  • this.$$.listeners: Event listeners (Automatically bound to VM)
    • e.g., v-on="$$.listeners" or v-on="{ ...$listeners, ...$$.listeners }"
//App.vue
<template>
  <div>
    <Parent>
      <Child />
    </Parent>
  </div>
</template>

<script>
import Parent from "./components/Parent.vue";
import Child from "./components/Child.vue";
export default {
  components: { Parent, Child },
};
</script>

The App component renders the Parent component and Child component within it:

Our vue-proxi demo

The count value can be incremented from both Parent and Child components.

Caveats with extending, wrapping, or proxying UI components

When extending, wrapping, or proxying UI components, there are a few caveats to keep in mind:

  1. Complexity: Adding an additional layer of abstraction through extension, wrapping, or proxying can increase the complexity of the codebase by introducing additional concepts, configuration, or dependencies that developers need to understand
  2. Performance: Depending on the implementation, there can be performance implications when extending, wrapping, or proxying UI components. Extra layers of indirection, additional logic, or inefficient implementations can impact the application’s overall performance, so it’s important to optimize and test the performance of these customizations
  3. Prop collision: Prop collision is possible when the extended or wrapped UI component and the original component have conflicting prop names or expect different prop values, which can lead to unexpected behavior or errors. Carefully consider and handle prop naming to ensure proper functionality
  4. Breaking changes in external components: When extending, wrapping, or proxying external UI components, future updates or breaking changes to those components may require updates to the extended or wrapped components as well. It’s important to stay updated with the changes in the external components and make the necessary adjustments to maintain compatibility
  5. Composition vs. inheritance: Extending or wrapping UI components can introduce a more inheritance-oriented approach, which may not always be the most suitable solution. Composition-based approaches comprised of components using slots or props provide more flexibility and can be easier to manage and maintain in the long run

It’s important to weigh the advantages and disadvantages of extending, wrapping, or proxying UI components based on the specific requirements and constraints of the project. Consider the trade-offs in terms of complexity, performance, prop collision, and compatibility to determine the most suitable approach for your particular use case.

Conclusion

This guide has exposed you to wrapping components, extending them, and creating proxies around them in Vue.js. These techniques enable developers to enhance functionality, control component behavior, and facilitate communication within the application. Understanding and implementing these techniques can lead to more maintainable and efficient codebases.

Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket. LogRocket Dashboard Free Trial Bannerhttps://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - Start monitoring for free.

Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply