Mixins merge their properties into the component, which can cause naming conflicts if multiple mixins use the same names. Composables are functions that return reactive state and logic, letting you use them without merging, so conflicts are avoided.
import { ref } from 'vue'; const useCounter = () => { const count = ref(0); const increment = () => count.value++; return { count, increment }; }; const counterMixin = { data() { return { count: 0 }; }, methods: { increment() { this.count++; } } }; export default { name: 'TestComponent', mixins: [counterMixin], setup() { const { count: compCount, increment: compIncrement } = useCounter(); return { compCount, compIncrement }; }, template: `<div> <p>Mixin count: {{ count }}</p> <button @click="compIncrement">Increment</button> </div>` };
The template uses {{ count }} from the mixin's data(), which stays at 0. The setup() renames composable properties to compCount and compIncrement. The button calls the composable's compIncrement, incrementing compCount but since it's not displayed, the visible count stays at 0.
Mixins declare lifecycle hooks as component options and Vue merges and calls them automatically. Composables use special functions like onMounted inside setup(), so they run only if called inside setup().
const myMixin = {
data() {
return {
message: 'Hello'
}
},
methods: {
greet() {
console.log(this.message)
}
}
}
export default {
name: 'MyComponent',
mixins: myMixin,
template: ``
}The mixins option expects an array of mixin objects. Writing mixins: myMixin passes an object instead of an array, causing a syntax error.
export function useSharedState() { const state = reactive({ count: 0 }); const increment = () => { state.count++ }; return { state, increment }; } // Component A and Component B both import and call useSharedState() separately
Each call to useSharedState() creates a new reactive object. So components get independent state copies. To share state, the reactive object must be defined outside the function or use a shared singleton pattern.