Skip to content

VueJS - Mixins

VueJS - Reusing Logic: Mixins and Composables

Section titled “VueJS - Reusing Logic: Mixins and Composables”

Code reuse is crucial for maintainable applications. Vue offers several ways to share logic between components. Historically, Mixins were a common approach in Vue 2. However, Vue 3’s Composition API provides a more flexible and explicit pattern called Composables.

Mixins are objects containing component options (like data, methods, created, etc.). When a component uses a mixin, the mixin’s options are ‘mixed into’ the component’s own options.

Let’s define a simple mixin that logs a message when a component is created:

// loggerMixin.js
export const loggerMixin = {
created() {
console.log(`Component ${this.$options.name || 'unnamed'} created via mixin.`);
if (this.logMessage) { // Access data property potentially defined in component
this.logMessage('Mixin logging activated!');
}
},
methods: {
alertMixinMessage(msg = 'Hello from mixin!') {
alert(msg);
}
}
};

Now, let’s use this mixin in a component (using Vue 2 style Options API for demonstration):

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Mixin Example</title>
<script src="https://unpkg.com/vue@2"></script> <!-- Using Vue 2 for mixin demo -->
</head>
<body>
<div id="app">
<my-component></my-component>
</div>
<script type="module">
// Assume loggerMixin is imported from './loggerMixin.js'
const loggerMixin = {
created() {
console.log(`Component ${this.$options.name || 'unnamed'} created via mixin.`);
if (this.logMessage) {
this.logMessage('Mixin logging activated!');
}
},
methods: {
alertMixinMessage(msg = 'Hello from mixin!') {
alert(msg);
}
}
};
const MyComponent = {
name: 'MyComponent',
mixins: [loggerMixin], // Use the mixin
data() {
return {
componentData: 'Some data specific to the component'
};
},
methods: {
logMessage(msg) { // Method defined in component
console.log(`Component Log: ${msg}`);
},
callMixinMethod() {
this.alertMixinMessage('Called from component!'); // Can call mixin methods
}
},
template: `<div>
<p>{{ componentData }}</p>
<button @click="callMixinMethod">Test Mixin Method</button>
</div>`,
mounted() {
console.log('Component mounted.');
}
};
new Vue({
el: '#app',
components: { MyComponent }
});
</script>
</body>
</html>

When this runs:

  • The created hook from loggerMixin runs.
  • The component’s mounted hook runs.
  • The component can access its own data (componentData).
  • The component can call methods defined within itself (logMessage).
  • The component can also call methods defined in the mixin (alertMixinMessage).
  • The mixin can potentially call methods defined in the component if they exist (like logMessage here).

Expected Console Output:

  1. Component Log: Mixin logging activated! (Mixin calling component’s method)
  2. Component MyComponent created via mixin. (Mixin’s created hook)
  3. Component mounted. (Component’s mounted hook)

Clicking the button will trigger an alert: Called from component!

When a component and its mixin(s) define overlapping options, Vue merges them based on specific strategies:

  • data: Merged recursively. Component’s data takes priority in case of conflicts at the root level.
  • Lifecycle Hooks (e.g., created, mounted): Merged into an array. All hooks (mixin first, then component) will be called.
  • Options like methods, components, directives: Merged into the same object. Component’s options take priority if keys conflict.
const conflictMixin = {
data() {
return { message: 'Mixin Message', commonValue: 1 };
},
methods: {
greet() { console.log('Hello from Mixin'); },
commonMethod() { console.log('Common method from Mixin'); }
},
created() { console.log('Mixin created hook'); }
};
const ConflictingComponent = {
mixins: [conflictMixin],
data() {
return { message: 'Component Message', specificValue: 2 }; // 'message' conflicts
},
methods: {
farewell() { console.log('Goodbye from Component'); },
commonMethod() { console.log('Common method from Component'); } // 'commonMethod' conflicts
},
created() { console.log('Component created hook'); },
mounted() {
console.log(this.message); // Output: Component Message (Component data wins)
console.log(this.commonValue); // Output: 1 (From mixin, no conflict)
console.log(this.specificValue); // Output: 2 (From component)
this.greet(); // Output: Hello from Mixin
this.farewell(); // Output: Goodbye from Component
this.commonMethod(); // Output: Common method from Component (Component method wins)
},
template: '<div>Conflicting Component</div>'
};
// In Vue instance:
// new Vue({ el: '#app', components: { ConflictingComponent }, template: '<conflicting-component />' });

Expected Console Output (on mount):

  1. Mixin created hook
  2. Component created hook
  3. Component Message
  4. 1
  5. 2
  6. Hello from Mixin
  7. Goodbye from Component
  8. Common method from Component

While mixins allow code reuse, they have significant drawbacks, especially in larger applications:

  • Implicit Dependencies: It’s unclear where properties (like this.message) or methods used within a component actually come from if multiple mixins are used.
  • Namespace Collisions: As seen, property and method names from different mixins or the component itself can clash, leading to unexpected behavior or requiring careful naming conventions.
  • Limited Reusability/Configuration: Mixins are not easily configurable with parameters when used. Passing data to a mixin often involves relying on specific data properties existing in the component.
  • TypeScript Support: Type inference with mixins can be complex and less reliable.

Vue 3’s Composition API introduces ‘Composable Functions’ (or ‘Composables’) as the primary pattern for encapsulating and reusing stateful logic. A composable is simply a function that leverages Vue’s Composition API (like ref, computed, watch, lifecycle hooks) to manage state and logic.

Composables address the drawbacks of mixins by:

  • Explicit Source: Values returned by a composable function must be explicitly destructured or assigned in the component, making their origin clear.
  • No Namespace Collisions: Returned values can be renamed if needed within the component.
  • Flexible Arguments: Composables are just functions, so they can accept arguments for customization.
  • Better Type Inference: TypeScript works much more reliably with composables.

Let’s create a composable that tracks the mouse position:

useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMousePosition() {
// State encapsulated and managed by the composable
const x = ref(0);
const y = ref(0);
// Logic encapsulated by the composable
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
// Lifecycle hooks managed by the composable
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
// Expose managed state as return value
return { x, y };
}

Now, let’s use this composable in a Vue 3 component using <script setup>:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Composable Example</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
Mouse position: x={{ mouseX }}, y={{ mouseY }}
</div>
<script type="module">
import { createApp, ref, onMounted, onUnmounted } from 'vue';
// Assume useMousePosition is imported from './useMousePosition.js'
function useMousePosition() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
createApp({
setup() {
// Use the composable
const { x, y } = useMousePosition();
// Rename if needed to avoid conflicts
// const { x: mouseX, y: mouseY } = useMousePosition();
// Return the state to be used in the template
// Using different names here for clarity in template
return {
mouseX: x,
mouseY: y
};
}
}).mount('#app');
</script>
</body>
</html>

In this Vue 3 example:

  • We import and call useMousePosition() inside the setup() function.
  • We destructure the returned x and y refs.
  • The source of x and y is explicitly the useMousePosition composable.
  • The logic for adding/removing the event listener is encapsulated within the composable.
  • The component only needs to consume the state provided by the composable.

While mixins exist and you might encounter them in older Vue 2 codebases, Composables are the recommended pattern for code reuse in modern Vue 3 development. They offer superior explicitness, flexibility, collision avoidance, and TypeScript support compared to mixins.

If migrating from Vue 2, refactoring mixins into composables is often a worthwhile effort for improved maintainability.

Further Reading: Vue’s official documentation on Composables: https://vuejs.org/guide/reusability/composables.html