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:
vue-proxi
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 { /* … */ }
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.
There are two primary methods to extend Vue components and add custom functionality to them:
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.
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.
There are simpler alternatives to extending Vue components that can achieve similar results without the complexity of component inheritance. Here are a few alternatives:
Let’s go into each of them.
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>
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.
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>
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
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.
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
.
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 proxyhandler
: an object that defines which operations to intercept and how to redefine intercepted operationsThe 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.
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.
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.
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
:
<proxi>
is reactive, ensuring that any changes to the injected data are automatically reflected in the componentUsing 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
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.
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
: Classthis.$$.props
: Props (Automatically bound to VM)this.$$.attrs
: Attributes
v-bind="$$.attrs"
or v-bind="{ ...$attrs, ...$$.attrs }"
this.$$.listeners
: Event listeners (Automatically bound to VM)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:
The count
value can be incremented from both Parent
and Child
components.
When extending, wrapping, or proxying UI components, there are a few caveats to keep in mind:
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.
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.
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]