How the Vue Composition API Replaces Vue Mixins
Looking to share code between your Vue components? If you’re familiar with Vue 2, you’ve probably used a mixin for this purpose. But the new Composition API, which is available now as a plugin for Vue 2 and an upcoming feature of Vue 3, provides a much better solution.
In this article, we’ll take a look at the drawbacks of mixins and see how the Composition API overcomes them and allows Vue applications to be far more scalable.
Mixins in a nutshell
Let’s quickly review the mixins pattern as it’s important to have it top-of-mind for what we’ll cover in the next sections.
Normally, a Vue component is defined by a JavaScript object with various properties representing the functionality we need — things like data
, methods
, computed
, and so on.
// MyComponent.js
export default {
data: () => ({
myDataProperty: null
}),
methods: {
myMethod () { ... }
}
// ...
}
When we want to share the same properties between components, we can extract the common properties into a separate module:
// MyMixin.js
export default {
data: () => ({
mySharedDataProperty: null
}),
methods: {
mySharedMethod () { ... }
}
}
Now we can add this mixin to any consuming component by assigning it to the mixin
config property. At runtime, Vue will merge the properties of the component with any added mixins.
// ConsumingComponent.js
import MyMixin from "./MyMixin.js";
export default {
mixins: [MyMixin],
data: () => ({
myLocalDataProperty: null
}),
methods: {
myLocalMethod () { ... }
}
}
For this specific example, the component definition used at runtime would look like this:
export default {
data: () => ({
mySharedDataProperty: null
myLocalDataProperty: null
}),
methods: {
mySharedMethod () { ... },
myLocalMethod () { ... }
}
}
Mixins are considered “harmful”
Back in mid-2016, Dan Abramov wrote “Mixins Considered Harmful” in which he argues that using mixins for reusing logic in React components is an anti-pattern, advocating instead to move away from them.
The same drawbacks he mentions about React mixins are, unfortunately, applicable to Vue as well. Let’s get familiar with these drawbacks before we take a look at how the Composition API overcomes them.
Naming collisions
We saw how the mixin pattern merges two objects at runtime. What happens if they both share a property with the same name?
const mixin = {
data: () => ({
myProp: null
})
}
export default {
mixins: [mixin],
data: () => ({
// same name!
myProp: null
})
}
This is where the merge strategy comes into play. This is the set of rules to determine what happens when a component contains multiple options with the same name.
The default (but optionally configurable) merge strategy for Vue components dictates that local options will override mixin options. There are exceptions though. For example, if we have multiple lifecycle hooks of the same type, these will be added to an array of hooks and all will be called sequentially.
Even though we shouldn’t run into any actual errors, it becomes increasingly difficult to write code when juggling named properties across multiple components and mixins. It’s especially difficult once third-party mixins are added as npm packages with their own named properties that might cause conflicts.
Implicit dependencies
There is no hierarchical relationship between a mixin and a component that consumes it. This means that a component can use a data property defined in the mixin (e.g. mySharedDataProperty
) but a mixin can also use a data property it assumes is defined in the component (e.g. myLocalDataProperty
). This is commonly the case when a mixin is used to share input validation. The mixin might expect a component to have an input value which it would use in its own validate method.
This can cause problems, though. What happens if we want to refactor a component later and change the name of a variable that the mixin needs? We won’t notice, looking at the component, that anything is wrong. A linter won’t pick it up either. We’ll only see the error at runtime.
Now imagine a component with a whole bunch of mixins. Can we refactor a local data property, or will it break a mixin? Which mixin? We’d have to manually search them all to know.
Migrating from mixins
Dan’s article offers alternatives to mixins, including higher-order components, utility methods, and some other component composition patterns.
While Vue is similar to React in many ways, the alternative patterns he suggests don’t translate well to Vue. So, despite this article being written in mid-2016, Vue developers have been suffering with mixin issues ever since.
Until now. The drawbacks of mixins were one of the main motivating factors behind the Composition API. Let’s get a quick overview of how it works before we look at how it overcomes the issues with mixins.
Composition API crash course
The key idea of the Composition API is that, rather than defining a component’s functionality (e.g. state, methods, computed properties, etc.) as object properties, we define them as JavaScript variables that get returned from a new setup
function.
Take this classic example of a Vue 2 component that defines a “counter” feature:
//Counter.vue
export default {
data: () => ({
count: 0
}),
methods: {
increment() {
this.count++;
}
},
computed: {
double () {
return this.count * 2;
}
}
}
What follows is the exact same component defined using the Composition API.
// Counter.vue
import { ref, computed } from "vue";
export default {
setup() {
const count = ref(0);
const double = computed(() => count * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
}
You’ll first notice we import a ref
function, which allows us to define a reactive variable that functions pretty much the same as a data
variable. Same story for the computed function.
The increment
method is not reactive so it can be declared as a plain JavaScript function. Notice that we need to change the sub-property value
in order to change the value of the count
reactive variable. That’s because reactive variables created using ref
need to be objects to retain their reactivity as they’re passed around.
It’s a good idea to consult the the Vue Composition API docs for a detailed explanation of how ref works.
Once we’ve defined these features, we return them from the setup function. There is no difference in functionality between the two components above. All we did was use the alternative API.
Tip: the Composition API will be a core feature of Vue 3, but you can also use it in Vue 2 with the NPM plugin @vue/composition-api
.
Code extraction
The first clear advantage of the Composition API is that it’s easy to extract logic.
Let’s refactor the component defined above with the Composition API so that the features we defined are in a JavaScript module useCounter
. (Prefixing a feature’s description with “use” is a Composition API naming convention.)
//useCounter.js
import { ref, computed } from "vue";
export default function () {
const count = ref(0);
const double = computed(() => count * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
Code reuse
To consume that feature in a component, we simply import the module into the component file and call it (noting that the import is a function). This returns the variables we defined, and we can subsequently return these from the setup function.
// MyComponent.js
import useCounter from "./useCounter.js";
export default {
setup() {
const { count, double, increment } = useCounter();
return {
count,
double,
increment
}
}
}
This may all seem a bit verbose and pointless at first, but let’s see how this pattern overcomes the issues with mixins we looked at earlier.
Naming collisions… solved!
We saw before how a mixin can use properties that may have the same name as those in the consuming component, or even more insidiously, in other mixins used by the consuming component.
This is not an issue with the Composition API because we need to explicitly name any state or methods returned from a composition function:
export default {
setup () {
const { someVar1, someMethod1 } = useCompFunction1();
const { someVar2, someMethod2 } = useCompFunction2();
return {
someVar1,
someMethod1,
someVar2,
someMethod2
}
}
}
Naming collisions will be resolved the same way they are for any other JavaScript variable.
Implicit dependencies… solved!
We also saw before how a mixin may use data properties defined on the consuming component, which can make the code fragile and very hard to reason about.
A composition function can also call on a local variable defined in the consuming component. The difference, though, is that this variable must now be explicitly passed to the composition function.
import useCompFunction from "./useCompFunction";
export default {
setup () {
// some local value the a composition function needs to use
const myLocalVal = ref(0);
// it must be explicitly passed as an argument
const { ... } = useCompFunction(myLocalVal);
}
}
Wrapping up
The mixin pattern looks pretty safe on the surface. However, sharing code by merging objects becomes an anti-pattern due to the fragility it adds to code and the way it obscures the ability to reason about the functionality.
The most clever part of the Composition API is that it allows Vue to lean on the safeguards built into native JavaScript in order to share code, like passing variables to functions, and the module system.
Does that mean the Composition API is superior in every way to Vue’s classic API? No. In most cases you’ll be fine to stick with the classic API. But if you’re planning to reuse code, the Composition API is unquestionably superior.
The post How the Vue Composition API Replaces Vue Mixins appeared first on CSS-Tricks.