What is the Composition API?
Vue 3 introduced the Composition API, which has since swept the entire community. In my opinion, the only best feature of the Composition API is the ability to extract reaction states and functions into their own reusable modules or "composable".
What is composable?
So, what is Vue.js Composable? You can almost think of composition as a hybrid API equivalent of the option API. They provide a way to define reactive data and logic separate from any particular component. Not only that, they are doing better... Much better. In addition, they have done more.
Similar problems solved by Composables and Mixin
Let's first look at how composable and hybrid are similar. Like mixin, composability allows us to extract reactive data, methods, and computational properties and easily reuse them across multiple components.
Composition vs Mixin
If the recyclable composition and mixer have the same purpose, why introduce the recyclable composition when we already have a mixer? 2 reasons:
- They can better solve the same problem
- They can solve more problems
Clarity of data / method sources
Mixins = data source ambiguity
The use of mixins will eventually mask the source of reactivity data and methods, especially when multiple mixins are used for a single component or one 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 reusable compositions, we can know exactly where our reusable data and functions come from. That's because we have to import composable items and explicitly use deconstruction 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) } }
name conflict
Mixins = risk of naming conflicts
Using the same mixin example above, what happens if two mixins actually define a name data attribute? The result will be that the data from the last mixin will win and the data in any other mixin will 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 = risk of no naming conflicts
However, this is not the case for the composable composition. Composables can expose data or functions with the same name, but the consumer component can rename these variables at will.
//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) } }
Response data from the variation module of the component
Usually, we want reusable modules (mixin or composable) to directly change the value of some reactive data without giving this ability to consumer components.
Mixins = unable to protect their reactivity data
Take the example of RequestMixin.
// 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 may not want the consumer component to change the loadingor value payload at will. However, it is impossible to use mixin. Mixin has no mechanism to protect data.
Composables = you can protect your reactivity data
Now compare it to the same logic written as 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 }; };
In this composable, we can change the values of loads and payloads at will, but once we expose them to any consumer component, we make them read-only. How sweet!
Composable global state
The last ability that can be combined is something mixin doesn't have. It's very cool. Maybe one of my favorite parts is that it's really simple. All data defined with mixins will always be reset for each new component instance that uses it.
//CounterMixins.js export default{ data(){ return { count: 0 } }, methods:{ increment(){ this.count ++ } } }
For the above mixin, the count of each component will always start from 0, and increasing the count in a component using mixin will not increase the count in another component using mixin.
We can use composable to achieve the same function.
//useCounter.js import {ref, readonly} from 'vue' export default () => { const count = ref(0) const increment = ()=> count.value++ return { count: readonly(count), increment } }
Many times, this is expected behavior. Sometimes, however, we want reactive data to be synchronized across all components and more like a global state defined in something like Vuex.
How do we use composable to do this? Through a simple line feed.
//useCounter.js import {ref, readonly} from 'vue' const count = ref(0) export default () => { const increment = ()=> count.value++ return { count: readonly(count), increment } }
Can you see the difference? All we have done is move the definition of to the count export function. Now every time increment is called, no matter which component it is called from, it references the same count variable, because count is defined outside the scope of the exported function.
There are many problems that can be solved. For example, you can have a useAuthUser composable or useCart composable. Basically, you can use this technique for any data that should be global throughout the application.
conclusion
In short, composable intent is usually the same as mixin's: Reusable logic can be extracted from components to achieve reusability. However, in practice, mixin will eventually fail to meet the requirements, but the composition can do this work well.