Vue 3 introduced the Composition API, which has since taken the community by storm. In my opinion, the single best feature of the Composition API is the ability to extract reactive state and functionality into their own reusable modules or "composables".
So, what is a Vue.js Composable? You can almost think of composables as the Composition API equivalent of mixins for the Options API. They provide a way to define reactive data and logic separate from any particular component. Not only that, but they do it better... much better. Plus, they also do a bit more.
Let's start by looking at how composables and mixins are alike. Just like mixins, composables allow us to extract reactive data, methods, and computed properties and easily reuse them between multiple components.
If composables and mixins serve the same purpose then why introduce composables when we already have mixins? 2 reasons:
Mixins = Data Source Obscured
Using mixins ends up obscuring the source of reactive data and methods, especially when multiple mixins are used for a single component or a mixin has been registered globally.
//MyComponent.vue
import ProductMixin from './ProductMixin'
import BrandMixin from './BrandMixin'
import UserMixin from './UserMixin'
export default{
mixins:[ProductMixin, BrandMixin, UserMixin],
created(){
// Where in the world did name come from?
// Let me look through each of the registered mixins to find out
// Oh man, it's not in any of them...
// It must be from a globally registered mixin
console.log(this.site)
// Oh, I got lucky here turns out the first mixin I inspected has the name
console.log(this.name)
}
}
Composables = Transparent Source of Data and Functions
However, using composables, we can tell exactly where our reusable data and functions come from. That's because we have to import the composable and then explicitly use destructuring to get our data and functions.
//MyComponent.vue
import useProduct from './useProduct'
import useBrand from './useBrand'
import useUser from './useUser'
export default{
setup(){
const { name } = useProduct()
return { name }
}
created(){
// Where in the world did name come from?
// ah, it's not in setup anywhere... this doesn't exist and is an error
console.log(this.site)
// Oh, nice I can see directly in setup this came from useProduct
console.log(this.name)
}
}
Mixins = Risk of Naming Collisions
Using the same mixin example from above, what if 2 of the mixins actually defined a name
data property? The result would be that the data from the last mixin would win and the data in any other mixin would be lost.
//MyComponent.vue
import ProductMixin from './ProductMixin' // name = AirMax
import BrandMixin from './BrandMixin' // name = Nike
import UserMixin from './UserMixin' // name = John Doe
export default{
mixins:[ProductMixin, BrandMixin, UserMixin],
created(){
// Actually I'm not so lucky,
// yeah I found the name in ProductMixin
// but turns out UserMixin had a name too
console.log(this.name) // John Doe
}
}
Composables = NO risk of naming collisions
This is not the case however, with composables. Composables can expose the data or functions with the same name but then the consuming component can rename those variables however it pleases.
//MyComponent.vue
import useProduct from './useProduct' // name = AirMax
import useBrand from './useBrand' // name = Nike
import useUser from './useUser' // name = John Doe
export default{
setup(){
const { name: productName } = useProduct()
const { name: brandName } = useBrand()
const { name: userName } = useUser()
return { productName, brandName, userName }
}
created(){
// Yay! Nothing is lost and I can get the name of each of the things
// together in my component but when writing the composables
// I don't have to think at all about what variable names might collide
// with names in other composables
console.log(this.productName)
console.log(this.brandName)
console.log(this.userName)
}
}
Usually we want the reusable module (the mixin or composable) to be able to directly change the value of certain reactive data without granting that ability to the consuming component.
Mixins = Can NOT Safeguard Own Reactive Data
Take a RequestMixin
for example.
// RequestMixin.js
export default {
data(){
return {
loading: false,
payload: null
}
},
methods:{
async makeRequest(url){
this.loading = true
const res = await fetch(url)
this.payload = await res.json()
this.loading = false
}
}
}
In this case, we probably do NOT want the consuming component arbitrarily changing the value of loading
or payload
. However, with mixins that's just not possible. Mixins have no mechanism of safeguarding that data.
Composables = Can Safeguard Own Reactive Data
Now compare that to the same logic written as a composable.
// useRequest.js
import { readonly, ref } from "vue";
export default () => {
// data
const loading = ref(false);
const payload = ref(null);
// methods
const makeRequest = async (url) => {
loading.value = true;
const res = await fetch(url);
payload.value = await res.json();
};
// exposed
return {
payload: readonly(payload), //notice the readonly here
loading: readonly(loading), // and here
makeRequest
};
};
Inside of this composable we can change the value of loading and payload anyway we'd like but as soon as we expose those to any consuming components we make them read only. Pretty sweet!
This last ability that composables have that mixins do not have, is a really cool one. Maybe one of my favorite parts about it is how simple it really is. With mixins all the data that is defined will always be reset for every new component instance it's used in.
//CounterMixins.js
export default{
data(){
return { count: 0 }
},
methods:{
increment(){
this.count ++
}
}
}
For the above mixin, every component's count will always start at 0 and incrementing the count in a component using the mixin will not increment the count in another component using the mixin.
We can achieve the same functionality with a composable.
//useCounter.js
import {ref, readonly} from 'vue'
export default () => {
const count = ref(0)
const increment = ()=> count.value++
return {
count: readonly(count),
increment
}
}
Often times this is the desired behavior. However sometimes, we'd like reactive data to sync across all components and act more like global state defined in something like Vuex.
How do we do this with a composable? With one simple line change.
//useCounter.js
import {ref, readonly} from 'vue'
const count = ref(0)
export default () => {
const increment = ()=> count.value++
return {
count: readonly(count),
increment
}
}
Can you spot the difference? All we did was move the definition of count
outside of the exported function. Now every time increment is called, no matter what component it's called from, it references the same count variable because count is defined outside of the scope of the exported function.
There are a numerous amount of problems this could be used to solve. For instance you could have a useAuthUser
composable or a useCart
composable. Basically, you can use this technique for any data that should be global throughout the application.
In conclusion, the intent of composables is often the same as the intent of mixins: to be able to extract reusable logic out of components for re-usability. In practice, however mixins just end up falling short while composables do the job with excellence.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.