How can Vue 3 composables make your life easier?

How can Vue 3 composables make your life easier?

Vue is an amazing tool for building reactive applications. But even the best tools have some disadvantages. Over the years of working with this amazing framework, I have always complained about one thing – the logic-sharing mechanism. In this article, I’ll explain what is wrong with Vue.js mixins and how Vue 3 solves all of their problems.

Here’s what you will learn from this article:

  • What are the disadvantages of the mixins;
  • What kind of bugs could happen when you share logic between Vue.js components;
  • What is composable and how to reuse the logic inside your web app by using it;
  • How to make your Vue.js app more predictable.

What are the disadvantages of the mixins?

As your projects grow and there is more and more code that you are proud of, you want to use pieces of your code in various components. Modern software is written without unnecessary repeats of the code responsible for app logic, even when it comes to front-end applications. The base mechanism that allows you to share logic in your Vue app are mixins. The mixin is an options object that you can define once and use in multiple places. It will be combined with component options and available in a component context. You can compare this concept to object-oriented programming and class extension; options defined by mixin extend your component functionality and you can access them via the “this” keyword. Sounds great, so what is the problem? Let's assume that you have the following mixins:

// Mixin 1 - userMixin
export default {
 data() {
   return {
     user: {
       name: '',
       age: '',
       score: 0,
     },
     wishlist: [],
   };
 },
 created() {
   this.getData()
 },
 methods: {
   async getData() {
     const url = `https://anyapi.com/api/user`;
     try {
       const response = await fetch(url);
       const data = await response.json();
       this.user = data.user;
       this.wishlist = data.wishlist;
     } catch (e) {}
   }
 }
};
// Mixin 2 - itemsMixin
export default {
 data() {
   return {
     items: [],
     category: ""
   };
 },
 methods: {
   async getData() {
     const url = `https://anyapi.com/api/${this.category}`;
     try {
       const response = await fetch(url);
       const data = await response.json();
       this.items = data
     } catch (e) {}
   }
 }
};

Let’s take a deep insight into the code of these mixins. The first one is the logic for fetching some user data and his wishlist. It fetches data as the component instance is ready and saves retrieved data to its own data structures.

The second one is the logic for API calls for fetching items by category. It provides a method to fetch data and data structures to store currently used categories and items. It's self-sufficient so everything looks great.

Nicely done? Not at all. As long as you use only one of these mixins in your Vue component, everything works properly. The request hits the backend service and fetched data is assigned where it should. But imagine if both of these mixins are used in one component, what will happen with the initial fetch of the user and what will happen if you call the getData method?

import userMixin from "../mixins/userMixin";
import itemsMixin from "../mixins/itemsMixin";
 
export default {
 mixins: [userMixin, itemsMixin],
 methods: {
   handleAction() {
     this.getData();
   }
 }
};

The answer is a bit tricky. Take a breath and dive into the details of the code. When our component is used, the created hook in the userMixin is called as component instance is set, but it calls the getData method from itemsMixin instead of userMixin. The reason for this is the order of mixins in the configuration array as well as the naming convention. Both mixins contain method getData and both are registered in specific order – userMixin as the first mixin and later on the itemsMixin mixin. When the instance of the component is created all options provided by mixins are mixed with component options. Because both mixins have methods named exactly the same, the second mixin in the array (itemsMixin) overwrites the one defined in the userMixin. Some people will say “Pff… take it easy, bro – let’s refactor the name of the methods and the code will be great again”, but as ultimate professional front-end dev you know it’s not ok after all. You don’t want to refactor mixin and all of the code areas where it was used previously. You just want to use the part of reusable code, and it’s not as simple as it should be.

There is also one more thing – take a look at userMixin. Data structures associated with mixinsOption hold user data and an additional wishlist array. What if you want to use only user data? It’s impossible because all of the options defined in mixin are mixed with component instances and there are no exceptions.

So what is the lesson here? The first disadvantage of the mixins is the unpredictability of the options name. You have to keep your eyes wide open and take care about the naming of every single option to avoid unpredictable behavior and extra bugs. The second disadvantage is a lack of control when it comes to logic provided to your Vue components. The scope of your component will be populated by options that you may not need in a specific situation. And there is one more thing – it's not so simple to find out where you should look for options provided by Vue.js mixins. You have to search registered mixins until you'll find the one that's responsible for a piece of logic. To be honest, such a mess is not a programmer’s greatest dream.

Sharing of the logic in Vue 3

As you learned in the previous chapter – working with reusable logic in Vue.js is not easy. Fortunately, Vue 3 changes the rules of the game and allows the use of a completely new mechanism to share logic between Vue components. All the nightmares after working all day with mixins will never come back. Let me introduce you to your new best friend  composables.

In the previous post, we told you what the composition API is and how to use it in your Vue project. Today I’ll explain to you how to use it to share logic between components in your wonderful apps. So what is composable? Composable is a simple function that returns pieces of logic such as data structures like refs, reactive objects, functions and all other possibilities of the composition API. It’s quite similar to mixins but there is a huge difference when it comes to controlling the data provided to your components.

The project structure convention states that logic reused with composables should be placed in the composables directory in your src. Each composable should be defined in a custom file with the use prefix, e.g.useUser, useItems, useApiCall etc. Let me show you composable based on userMixin:

// useUser
import { ref, reactive } from 'vue'
 
const useUser = () => {
 const user = reactive({
   name: '',
   age: '',
   score: 0,
 })
 
 const wishlist = ref([]);
 
 async function getData() {
   const url = `https://anyapi.com/api/user`;
   try {
     const response = await fetch(url);
     const data = await response.json();
     Object.assign(user, data.user)
     wishlist.value = data.wishlist;
     internalHelper();
   } catch (e) {}
 }
 
 // all of the things that you want to do in created hook should be called inside function body
 getData()
 
 return {
   user,
   wishlist,
   getData,
 }
}
export default useUser;

And for itemsMixin:

// useItems
import { ref } from 'vue'
 
const useItems = () => {
 const items = ref([]);
 const category = ref('');
 
 async function getData() {
   const url = `https://anyapi.com/api/${category.value}`;
   try {
     const response = await fetch(url);
     const data = await response.json();
     items.value = data
   } catch (e) {}
 }
 
 return {
   items,
   category,
   getData,
 }
}
export default useItems;

Take a look at the code of these Vue composables. As you can see, all of the logic from userMixin and itemsMixin are rewritten to modern and well-structured code (thanks to Vue 3 composition API, all of us love you). When it comes to sharing the logic, the most important part here is the return statement. You can strictly decide which data, methods, and other chunks of reusable logic will be exposed and possible to consume in the component where this logic will be implemented. So, if you need some helper to calculate something internally, you can easily use it but as long as you won’t return it in composable it can’t be accessible outside. This is how you regained control of the shared logic exposition. But wait, what’s about naming your functions? Let's solve this issue.

As I previously mentioned – the composables will give you full control over both the exported logic and the logic that you want to consume in your components. So let’s see how you can consume both methods without refactoring your pretty code:

import useUser from '../composables/useUser'
import useItems from '../composables/useItems'
 
const { getData: getUserData, user } = useUser();
const { getData: getItemsData, items, category } = useItems();
 
function handleAction() {
 getItemsData()
}
 
function refreshUser() {
 getUserData()
}

So let’s see how to consume the data and methods from composables. As composable is a function that always returns an object you have to invoke it inside your setup function or script with type setup keyword. It’s good practice to instantly use object destructuring to get access to the data that you want to use inside your component. And that's where the magic trick comes in – because of this destructuring, you can decide what data will be consumed, and you can even rename it to avoid naming conflicts with actually declared statements. So you don't need to consume a wishlist as it’s redundant inside this component.

Finally, another trick is worth mentioning. Imagine the situation when you want to pass some extra configuration to the shared logic. Do you remember the useItems mixin? The API call is related to category, what if you want to set the initial value of the category inside your component?

     const url = `https://anyapi.com/api/${this.category}`;
     try {
       const response = await fetch(url);
       const data = await response.json();
       this.items = data
     } catch (e) {}

Once again it’s not impossible but more complex than it is with composables. You have to set the value inside the created hook in your component:

created() {
   this.category = 'food'
 },

And take a look at how to provide some config with composable:

// useItems
import { ref } from 'vue'
 
const useItems = (initialCategory = '') => {
 const items = ref([]);
 const category = ref(initialCategory);
 
 async function getData() {
   const url = `https://anyapi.com/api/${category.value}`;
   try {
     const response = await fetch(url);
     const data = await response.json();
     items.value = data
   } catch (e) {}
 }
 
 return {
   items,
   category,
   getData,
 }
}
export default useItems;

Your composable is nothing more than a JS function, so you can set parameters that will be used as the initial value of the data structure defined inside it. You can even define default values for it. Every call of this function is independent so you can use it multiple times even in the same component. To simplify, it's more like functional programming. Let’s check how to provide an argument to this composable – simply pass defined data at the initial call of your function:

const { getData: getItemsData, items, category } = useItems('food');

Let’s sum it up – Vue 3 composables give you much more control over your code. You can strictly decide what data and methods should be exposed and consumed when you work with reusable logic.

Summary

The previous version of the Vue.js provides some features that allow you to share logic between components of your web application. Suddenly it wasn’t perfect, because data provided by mixins was out of your control. With the launching of the new version of Vue.js the composition API and composables were introduced to improve control over your code. Composables are game changers when it comes to the sharing of logic. The lesson of the code sharing was learned by a core team of the Vue.js and all of the weakness of the mixins was fixed by a composable mechanism. So keep it in your mind that Vue 3 should be a perfect pick for your next large app.

The final thing worth mentioning is TypeScript support. Sharing of the logic with composables mechanism ensures perfect type checks and hints that for sure improve your development experience.


If you would like to know more about Vue.js check out our series of the Vue 3 articles: