What is a Vue.js Composable?

Written by Daniel Kelly

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".

What is a Composable?

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.

Similar Problems Composables and Mixins Solve

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.

code comparison between mixin and composable

Composables vs Mixins

If composables and mixins serve the same purpose then why introduce composables when we already have mixins? 2 reasons:

  1. They can solve the same problems better
  2. And they cans solve even more problems

Clarity of Data/Methods Source

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)
    }
}

Naming Collisions

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)
    }
}

Mutating Module's Reactive Data from the Component

Learn Vue.js 3 With Vue School

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!

Global State with Composables

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.

Conclusion

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.

Learn Vue.js 3 With Vue School

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

Building a Multi-Step Form with Petite-Vue

Building a Multi-Step Form with Petite-Vue