Skip to the solution
MediumFrontend9 min read

Why are composables preferred over mixins?

516 views

The problem

Question presented to candidate: "We have a legacy component that pulls in four mixins, and nobody is sure where count or reset() comes from. Why does the Vue team recommend composables over mixins, and how would you refactor this?"

What a strong answer should cover:

  • Mixins merge options into the component, so their data and methods appear on this with no trace of where they came from.
  • Name collisions are silent: for data and methods, a later mixin overwrites an earlier one and the component overrides both, with no warning.
  • Implicit dependencies: a mixin can read this.something that it expects the component (or another mixin) to define.
  • The data merge is shallow in Vue 3, and lifecycle hooks from all mixins run (mixins first).
  • Composables fix each problem: explicit imports, renaming on destructure, dependencies passed as arguments, and types that infer.
  • Mixins still work in Vue 3, and global mixins (app.mixin) are worse because they touch every component, including third-party ones.

Clarifying questions expected:

  • "Are any of these mixins global, or only local to this component?"
  • "Do other components depend on the same mixins? That decides whether to extract shared composables first."

Code / implementation expected: Yes β€” a reproduction of a mixin name collision and the composable rewrite that removes it.

  • mixins
  • composables

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 11 sections

Read the code

VueTwo mixins that both define count and reset, next to the same features as composables
<script setup>
import { ref, h, defineComponent } from "vue";

// ── Two mixins written by two different people. Both happen to use "count" and "reset".
const cartMixin = {
  data() { return { count: 3 }; },              // items in the cart
  methods: { reset() { this.count = 0; console.log("[cartMixin] reset"); } },
  mounted() { console.log("[cartMixin] mounted, count =", this.count); },
};
const notificationsMixin = {
  data() { return { count: 7 }; },              // unread notifications
  methods: { reset() { this.count = 0; console.log("[notificationsMixin] reset"); } },
  mounted() { console.log("[notificationsMixin] mounted, count =", this.count); },
};

const WithMixins = defineComponent({
  mixins: [cartMixin, notificationsMixin],
  mounted() { console.log("[component] mounted, count =", this.count); },
  render() {
    return h("div", [
      h("p", `mixins: count = ${this.count} (cart or notifications?)`),
      h("button", { id: "mreset", onClick: this.reset }, "reset"),
    ]);
  },
});

// ── The same two features as composables. Names are chosen at the call site.
function useCart() {
  const count = ref(3);
  return { count, reset: () => (count.value = 0) };
}
function useNotifications() {
  const count = ref(7);
  return { count, reset: () => (count.value = 0) };
}

const { count: cartCount, reset: clearCart } = useCart();
const { count: unread, reset: markAllRead } = useNotifications();
</script>

<template>
  <WithMixins />
  <p>composables: cart = {{ cartCount }}, unread = {{ unread }}</p>
  <button id="cclear" @click="clearCart">clear cart</button>
  <button id="cread" @click="markAllRead">mark all read</button>
</template>
VueMixin merge rules: the component wins, the merge is shallow, and hidden dependencies fail quietly
<script>
const userMixin = {
  data() { return { user: { id: 1, name: "mixin" }, theme: "dark" }; },
  computed: { greeting() { return "Hi " + this.userName; } }, // depends on something it never declares
};
export default {
  mixins: [userMixin],
  data() { return { user: { id: 2 } }; },   // component wins, and the merge is shallow
  mounted() { console.log("user =", JSON.stringify(this.user), "| theme =", this.theme, "| greeting =", this.greeting); },
};
</script>
<template><p>{{ user.id }}</p></template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 48 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What is defineExpose() and why are <script setup> components closed by default?

Continue