codamit.dev

Vue.js Mixins are broken

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 {
  mixins: [UserMixin],  data: () => ({
    isVisible: false,
  }),
  methods: {
    toggle() {
      this.isVisible = !this.isVisible;
    },
  },
};

Which result in the following runtime component definition.

export default {
  data: () => ({
    users: [],    isVisible: false,
  }),
  methods: {
    async getUsers() {      const response = await fetch('/api/users');      this.users = await response.json();    },    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 method doSomething(), it will break. The last declared mixin wins. Also the component 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>
  <section>
    <div v-for="user in users" :key="user.id">{{ user.name }}</div>
  </section>
</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 big downside is that we can not compose or share logic that rely on Vue rendering, 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 also known as hooks.

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>
  <section>
    <div v-for="user in users" :key="user.id">{{ user.name }}</div>
  </section>
</template>

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

  export default {
    setup() {
      const users = useGetUsers();
      return { users };
    },
  };
</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 compose with hooks to create complex UIs with a higher code reusability.

I'm Edouard Bozon, I play almost everyday with web technologies. I focus my work on building better JavaScript apps and contributing to open source. I'm freelancer, currently working on awesome projects.