Why I don't like Vue.js mixins

August 01, 2020

Vue.js comes with its mixin concept to share logic between components, this way we can extract common properties into a separate module.

export default {
  data: () => ({
    users: [],
  }),
  methods: {
    async getUsers() {
      const response = await fetch('/api/users');
      this.users = await response.json();
    },
  },
};

Then we can extend our component to use this mixin.

import UserMixin from './user-mixin';

export default {
  // highlight-start
  mixins: [UserMixin],
  // highlight-end
  data: () => ({
    isVisible: false,
  }),
  methods: {
    toggle() {
      this.isVisible = !this.isVisible;
    },
  },
};

Which result in the following runtime component definition.

export default {
  data: () => ({
    // highlight-start
    users: [],
    // highlight-end
    isVisible: false,
  }),
  methods: {
    // highlight-start
    async getUsers() {
      const response = await fetch('/api/users');
      this.users = await response.json();
    },
    // highlight-end
    toggle() {
      this.isVisible = !this.isVisible;
    },
  },
};

At the beginning the mixin pattern seem to work well for sharing code, but it quickly shows its limits. Let’s see why.

Mixins break encapsulation

It’s possible that some mixins cannot be used together. For example if two mixins declare the same doSomething() method, it will break because the last declared mixin wins. In this case the component also cannot define its own doSomething() method.

It’s quite difficult to fix name collisions because we need first to refactor the mixin, then find and refactor all the consuming components and mixins, which can be very complicated in a big project.

Once used a mixin is hard to change, hard to refactor.

Complexity snowballing

It’s impossible to know from the template part if a property or a method comes from the component itself or from a mixin. The relation between components and mixins is implicit.

It increases the mental charge to find what’s wrong if there is a bug. Where does the problem come from? The component? A mixin? Which one?

We’d have to manually search them all to know.

The more we use mixins in a project the more it becomes hard to reason about how components and mixins are coupled.

What alternatives do we have?

👍🏼 Using an ES module

The naive solution, if the function isn’t doing anything Vue specific, is simply using an ES module to share your code across components.

export async function getUsers() {
  const response = await fetch('/api/users');
  return await response.json();
}

Then we can just import it in our components.

<template>
  <div>
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
  </div>
</template>

<script>
  import { getUsers } from './get-users';

  export default {
    data: () => ({
      users: [],
    }),
    async mounted() {
      this.users = await getUsers();
    },
  };
</script>

This way we can easily test our getUsers function, because there’s no need to create a test component using @vue/test-utils.

The downside is that we can not share any logic that rely on Vue, which is very limiting.

👍🏼👍🏼 Using the composition API

The new fancy way to share code between components is using the composition API which is compatible with both Vue 2 and 3.

Note that everything related to composition is just an addition to the Vue API, which means that you can incrementally build with the Composition API.

After the library gets correctly installed we can create reusable chunks of code as the following.

import { ref, onMounted } from '@vue/composition-api';

export const useGetUsers = () => {
  const users = ref([]);

  onMounted(async () => {
    const response = await fetch('/api/users');
    users.value = await response.json();
  });

  return { users };
};

Now inside the setup function I can call the useGetUsers function and bind it to the view.

<template>
  <div>
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
  </div>
</template>

<script>
  import { useGetUsers } from 'use-get-users';

  export default {
    setup() {
      return { ...useGetUsers() };
    },
  };
</script>

Everything inside my useGetUsers is encapsuled which means that no name collision can happen. With composition, relations are explicit. I easily know where a property come from.

import { useFeatureA } from 'use-feature-a';
import { useFeatureB } from 'use-feature-b';

export default {
  setup() {
    return { ...useFeatureA(), ...useFeatureB() };
  },
};

As you can see it’s pretty trivial to use composition to create complex UIs with a higher code reusability.

Blog ideas

Upvote what you'd like me to write about next.

  • Nx DTE vs CI job matrix
    Nx Distributed Task Execution (DTE) vs a traditional CI job matrix with manual sharding: a comparison of developer experience, performance, and reliability.
  • Are you ready for a monorepo?
    An opinionated checklist to help you decide whether a monorepo is the right choice for your organization, and how to prepare for the transition.
  • Why monorepos unlock AI coding agents
    Shared context from frontend to backend, consistent conventions, and reliable task graphs: what makes a monorepo the ideal substrate for AI-assisted development.
  • Managing CI flakiness at scale
    Detecting, quarantining, and fixing flaky tests without slowing the pipeline: patterns that survive past 25 engineers.
  • Monitoring CI health
    Tools and techniques for tracking CI performance and reliability over time, and alerting on regressions before they impact developers.
Edouard Bozon

I'm Edouard Bozon, a passionate software engineer based in France, specializing in monorepo architectures, platform engineering, DevOps, and infrastructure. I build tools and workflows that help engineering teams scale their codebases and ship software with confidence.