Spiria logo.

Custom Vue 2 mixins inspired by Vuex helpers

May 18, 2023.

Mixins are a way to reuse common logic across multiple components, which reduces code duplication. Mixins were originally popularized by the React framework as an alternative to composition, then later used by other frameworks such as Vue. They were widely used at first, but the problems they created down the line made them problematic as projects grew. Nowadays, React and Vue 3 strongly discourage mixins in favor of composition.

Below, I first illustrate how common logic can be shared using services with standard dependency injection. I then show how mixins can do the same while also reducing boilerplate code in Vue 2 components. I briefly recap the most common pitfalls associated with mixins. Finally, I describe our custom approach in a Vue 2 application. Our solution, consisting of custom mixin-like helpers, output computed properties across components, much like Vuex.

1. Sharing common logic using services and dependency injection

Let’s look at a very basic Vue Single-File component:

<template>
    <div class="container">
        <input id="message-text-input" v-model="message.text" :disabled="isMessagePublished"/>
        <button id="publish-message-button" @click="publishMessage" :disabled="isMessagePublished">
            {{ this.language === "fr" ? "Publier" : "Publish" }}
        </button>
    </div>
</template>

<style scoped>
    .container {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }

    #message-text-input {
        width: 100%;
        border-radius: 12px;
    }

    #publish-message-button {
        width: 100%;
        height: 40px;
        border-radius: 12px;
        margin-top: 10px;
    }
</style>

<script>
import { mapState, mapActions } from "vuex";
import MessagesService from "../services/messages";

export default {
    name: "MessageComponent",
    computed: {
        ...mapState("profile", ["language"]),
        ...mapState("messages", ["message"]),
        // ...mapGetters("messages", ["isMessagePublished"]),
        ...mapActions("messages", ["publishMessage"]),
        isMessagePublished() {
            return MessagesService.isMessagePublished(this.message);
        }
    },
    methods: {
        async publishMessage() {
            if(!this.isMessagePublished) {
                await this.publishMessage();
            }
        }
    }
};
</script>

This simple component displays a current message object from a Vuex store. It allows message editing, then publishes it with a button using a store action, but only if the message is not yet published.

To avoid showing the whole Vuex store code, the isMessagePublished computed property replaces the store getter, though the store getter logic is the same. Since the concepts in this article apply to all computed properties with reusable logic across the application, just imagine it is another computed property related to store logic.

The message service could look something like this:

export class MessagesService {

    constructor() {}

    isMessagePublished(message) {
        return message.status === "PUBLISHED";
    }

    async getMessage(messageId) {
        // ...
    }

    async publishMessage(message) {
        // ...
    }
}

export default new MessagesService();

Following the “separation of concerns” design principle, the isMessagePublished method is implemented in the service in such a way that all message business logic is centralized in the service layer. This avoids cluttering our Vuex store with unrelated business logic. Here of course the logic is so simple that it could very well live in the store getter, but since we are dealing with business logic that is not necessarily related to store data, it makes more sense to keep it all in the same place, in the service layer.

2. Using mixins to help reduce boilerplate code in components

Now imagine that you have a more complex computed property used by multiple components. Even with the logic already centralized in MessageService, you would still need to add this duplicate computed property across multiple components. To avoid retyping this boilerplate code, you could define a custom Vue 2 mixin function and use it like this:

<script>
import { mapState } from "vuex";
import MessagesService from "../services/messages";

const mixin = {
    computed: {
        isMessagePublished() {
            return MessagesService.isMessagePublished(this.message);
        }
    }
}

export default {
    name: "MessageComponent",
    mixins: [mixin],
    computed: {
        ...mapState("profile", ["language"]),
        ...mapState("messages", ["message"]),
        ...mapActions("messages", ["publishMessage"])
    },
    methods: {
        async publishMessage() {
            if(!this.isMessagePublished) {
                await this.publishMessage();
            }
        }
    }
};
</script>

Note that the mixin function here would be defined in a mixin layer file, reusable across multiple components (e.g.: mixins/messages).

Though it reduced boilerplate code across multiple components, this new mixin also introduced some new difficulties:

  1. The this.message dependency of our mixin function is now hidden. This makes refactoring components a lot more difficult. For example, if we rename the message property from store as currentMessage in our component, we could easily forget to update it in our mixin function. Also, suppose you define a new mixin whose computed property depends on onefrom another mixin in order to function: this could quickly become unmanageable.
  2. If we try to use our new mixin inside a component with an existing isMessagePublished computed property, the names would conflict. Vue would silently resolve this according to your current merge-strategy configuration. This complicates the debugging because you have to track down which version is executed at runtime. As more mixins are added, including those from external dependencies, the potential for merge conflicts grows exponentially, making debugging even more difficult.

It might not be an issue in our very simple example, but as the complexity of the project and the codebase grows, the problems can snowball. This is especially true for large teams where team members are not aware of all mixins, their usage, and their dependencies across all the components and external plugins of the application.

3. Using custom mixins as helpers for common computed properties across components

For our Vue 2 application, we wanted to avoid those pitfalls while still retaining some of the benefits of mixins. We couldn’t use Vue 3’s new composition API at the time, so we implemented a custom solution.

As you might have noticed in our first example without mixins, using a store getter replaces the computed property by making use of the already centralized store logic, even if that logic still resides in the service layer. A Vuex helper method reducing boilerplate code could solve our problem without resorting to mixins. Building on this idea, we created custom helper functions to import our reusable mixin-like computed properties across our components, in the same way that Vuex store helper functions do.

Defining a new mapComputed mixin helper, our component now looks like this:

<script>
import { mapState } from "vuex";
import { mapComputed } from "../mixins";

export default {
    name: "MessageComponent",
    props: {
        message: {
            type: Object,
            required: true
        }
    },
    computed: {
        ...mapState("profile", ["language"]),
        ...mapState("messages", ["message"]),
        ...mapActions("messages", ["publishMessage"]),
        ...mapComputed("messages", ["isMessagePublished"])
    },
    methods: {
        async publishMessage() {
            if(!this.isMessagePublished) {
                await this.publishMessage();
            }
        }
    }
};
</script>

With the new helper function defined like this:

import messagesMixins from "./messages";

const mixins = {
    messages: messagesMixins
};

// Helper to select only needed computed properties
export const mapComputed = (moduleName, names = []) => {
    const computedProperties = {};
    const mixinModule = mixins[moduleName];
    if(!mixinModule) {
        throw new Error(`Invalid mixin module name: "${moduleName}"`);
    }
    if(!mixinModule.computed) {
        throw new Error(`Mixin module "${moduleName}" has no computed mixins`);
    }

    names.forEach(name => {
        const computed = mixinModule.computed[name];
        if(!computed) {
            throw new Error(`Mixin module "${moduleName}" has no computed mixin named "${name}"`);
        }

        computedProperties[name] = computed;
    });

    return computedProperties;
};
import MessagesService from "../services/messages";

/*
    Required data (see corresponding sections below)
*/
const messagesMixins = {
    computed: {
        /*
            Required data: this.message
        */
        isMessagePublished() {
            return MessagesService.isMessagePublished(this.message);
        }
    }
};

export default messagesMixins;

This solution had the following advantages:

  1. All our mixin-like computed properties are not only explicitly imported by name, they are also name-spaced like our Vuex stores, which made tracking name conflicts in components much easier and less risky. We still had to match all internal dependencies, but we diligently added comments with a clear list of dependencies to all our custom “mixins”, which makes them more obvious and will facilitate any eventual refactoring.
  2. It helped us centralize all our front-end business logic in our mixin layer. This made it much easier to migrate from an old Angularjs application with scattered front-end business logic. Normally, most of this logic, if not all, would have been handled by back-end or service layers before returning data, but this was not possible in our case without also recoding all our back-end API.

While it doesn’t solve all the problems introduced by mixins, this custom approach worked very well for our purposes, as the remaining front-end business logic after migration could be defined into very small functions.

We also had some computed properties that included results derived from previously computed properties. To avoid listing all sub-dependencies across our computed properties, we defined reusable private methods in our mixin files. This let us reuse common logic while importmaking each of our mixin-like computed properties importable independently. Given that our application was not very involved, we simply had to make sure that all the mixin dependencies were met in our components.

We also tried this approach with a similar mapMethods helper function, but method logic and dependencies quickly goes out of hand. In those cases, it made more sense to reduce the code duplication by centralizing the common logic in a normal service instead.

Summary

While still useful for sharing simple logic, mixins introduce more problems than they solve as the complexity of the code grows.

Vue 3’s new composition API, also available as a plugin for Vue 2, now provides a better way to reuse state logic in components. However, if this is not an option for your Vue 2 codebase, the custom approach shared here offers some alternatives for reusing common stateless logic in your components, without relying on mixins or cluttering your Vuex store.

For more information on mixins, their pitfalls and some alternatives, here is some useful reading: