Composition API in VueJS


Image by freepik

Hello guys! In this article I will talk about the Composition API in VueJS. More precisely, I’ll give you a brief introduction and talk about my experience with the Compositions API in my latest project and some of my thoughts about it.

So, let’s start!

Creating a component with some logic

VueJS uses the Options API, which can be summarized in: components, props, methods, data, computed and the life cycle hooks. So, in order to add behavior in a component you must use the available options.

If you want to create a component that searches and orders a list of users, you will end up with something like this:

const MyComponent = {
  name: 'MyComponent',
  data() {
    return {
        // search properties
        // sort properties
    };
  },
  computed: {
        // search properties
        // sort properties
  },
  methods: {
        // search properties
        // sort properties
  },
};

There are some drawbacks since you’re sharing data, computed, and methods with two different logics (search and sort) and both logics will be spread all over this component, decreasing your component maintainability and legibility.

Also, if you need to add a new feature, the component logic will end up getting more complex and this may cause some confusion or even bugs since now you have to handle three logics and one of them can influence the others because they’re not isolated.

Wouldn’t it be better to isolate each of these logics in their own functions? This way they could be easily reused, maintaned or understood. And that’s why we have the Composition API!

But, before we dive into it, you might think that you can use mixins, right? So, let’s talk a little bit about mixins and why you should avoid them.

Mixins

According to the VueJS Documentation:

Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.

Mixins allow you to extract the components logic into a single plain object with the Vue methods:

const myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

// define a component that uses this mixin
const Component = Vue.extend({
  mixins: [myMixin]
})

const component = new Component() // => "hello from mixin!"

But you might encounter some problems like being too hard to know which properties are being injected in the component and in order to grasp that you will probably end up opening and reading all mixins that your component is using. For example:

const Component = Vue.extend({
    mixins: [onWindowResize],
})

It’s kind of obvious that this mixin will add a listener when the window resizes but what is not obvious is how to use that. You might end up having to read the documentation of this mixin or open the implementation and figure it out.

Also, you can have conflicting properties since it’s hard to know which properties are being injected. For example:

const mixin1 = {
    data() {
        return {
            name: 'Felipe',
        }
    }
}

const mixin2 = {
    data() {
        return {
            name: 'Carlos',
        }
    }
}

const MyComponent = new Vue({
    mixins: [mixin1, mixin2],
    template: '<h1>{{ name }}</h1>',
})

In this example, both mixins are declaring a name inside the data method. Do you know which name will be rendered? In this case Carlos will be rendered because it’s the last mixin declared but the main problem is that the first mixin is being overwritten by the last one.

Both of those mixins are very simple but in the real world we’ll have more complex ones and a single conflict can lead to bugs they’ll be hard to find. But, lucky us, now we have the Composition API.

The Composition API

The Composition API allows us to define a specific behavior for a component with a simple function where it may return some properties to use in it. For those who came from React apps, it’s very similar to hooks and to be honest I found the Composition API searching for “vue hooks” on Google 😛

It’s common to create a component that will have more than one logic inside. The problem is that it’s hard to know which methods, data or computed props are being used by each logic or what the purpose is.

Imagine this scenario: you must create a new component that will have a certain behavior that another component already has. You’ll probably follow these steps:
1. Find the component with the desired behavior;
2. Copy this component;
3. Paste it in a new file;
4. Remove undesired code.

Also, you can refactor this code because it’s not a good code but this will probabily never be applied in other components that have the same code. And to be honest, at this point it will be hard to know which components are using the same logic because the code is spread over many components and you can have diferent methods/data/computed props names in each of them.

So, it’s not easy to reuse, refactor or even find which component has a specific behavior.

Here’s a simple example of how your component will work with the Composition API. You just need to declare a simple function and use it in the setup method inside your component:

const useSearchUsersByName = () => { ... }

const MyComponent = new Vue({
    setup() {
        const { searchUsersByName, users, isLoading } = useSearchUsersByName()

        return { searchUsersByName, users, isLoading }
    }
})

This example is providing you with a perspective on how to use the composition. Since it’s just a function that will return data, it’s easy to interact with it because you know which arguments and props will be returned. If you’re using an editor with the auto-complete feature, like VS Code, it will most likely help you.

Then, in this component we’ll have three props and by the names you can probably guess:
searchUsersByName -> is a method that you’ll pass an string with the name of the user
users -> is an array of users that match the searched name
isLoading -> is a boolean to know if the compositions is requesting (or calculating) the data

And of course you can reuse useSearchUsersByName composition in any component you want to. You can also use more than one composition in every component. And since your compositions don’t add methods, data or computed props in your component, they will never conflict with each other.

Also, it’s easy to know which components are using this logic because all you need to do is just search for this composition in the project and if you wish to refactor it, you just have to do it in one place.

When all your function does is return some props (or not) testing it is way more simple than testing the whole component.

Creating your first composition

As a first example I’ll create a simple counter that displays the current counter number and you can increment or decrement it.

In order to do that without the Composition API you must:
– Create a counter property in your component data as a number;
– Create an increment method;
– Create a decrement method;
– Use them in the template/view.

const CounterComponent = {
    data() {
        return {
            counter: 3,
        }
    },
    methods: {
        increment() {
            this.counter = this.counter + 1
        },
        decrement() {
            this.counter = this.counter - 1
        }
    },
    template: ' // I did not use backticks here due to WordPress formatting
        <div>
            <h2>Counter: {{ counter }}</h2>

            <button @click="increment">Increment</button>
            <button @click="decrement">Decrement</button>
        </div>
    '
}

This will work perfectly! I’ll be sharing a JSFiddle for each example so you can see this working in your machine.

JSfiddle: https://jsfiddle.net/nolleto/qgx89dkL/18

Now, let’s refactor this to a composition. As I mentioned before, compositions are just functions that may return some properties:

const useCounter = () => {
    let counter = 3

    const increment = () => {
        counter = counter + 1
    }

    const decrement = () => {
        counter = counter - 1
    }

    return { counter, increment, decrement }
}

And refactor the component:

const CounterComponent = {
  setup() {
    const { counter, increment, decrement } = useCounter()

    return { counter, increment, decrement }
  },

  template: ' // I did not use backticks here due to WordPress formatting
        <div>
          <h2>Counter: {{ counter }}</h2>

            <button @click="increment">Increment</button>
            <button @click="decrement">Decrement</button>
        </div>
    '
}

JSfiddle: https://jsfiddle.net/nolleto/b28orp10/21/

If you open the JSfiddle you’ll notice that this won’t work properly because everytime you click on Increment or Decrement, the counter will remain the same.
This happens because the counter variable is not reactive (let counter = 3). In order to make it reactive you need to use the Compostion API.

Before I go on, it’s important to know that if you are using Vue 3, you can get the compositions methods from the vue package. If you are using Vue 2, you must install the @vue/composition-api package.

So, let’s make this useCounter composition works!

import { ref } from 'vue' // if you are using Vue 3
import { ref } from '@vue/composition-api' // if you are using Vue 2

const useCounter = () => {
    const counter = ref(3)

    const increment = () => {
        counter.value = counter.value + 1
    }

    const decrement = () => {
        counter.value = counter.value - 1
    }

    return { counter, increment, decrement }
}

JSfiddle: https://jsfiddle.net/nolleto/ms8zf5e6/2/

It’s working now!

You have other methods at your disposal to help you create your composition like reactive, watch, computed and so on. You also have the lifecycle hooks like onBeforeMount, onMounted, onBeforeUpdate, etc. You can check all lifecycle hooks here.

Compositions that I made in my repos

I will show you some compositions that I’m currently using in my projects. They may be helpful to you or might end up helping you undestand what you can achieve with compositions.

useToggle

import { ref } from '@vue/composition-api';

const useToggle = (initialValue = false) => {
    const value = ref(initialValue)

    const toggle = () => {
        const newValue = !value.value

        value.value = newValue
    };

    const setAsTrue = () => {
        value.value = true
    };

    const setAsFalse = () => {
        value.value = false
    };

    return { value, toggle, setAsTrue, setAsFalse }
};

export default useToggle;

This seems to be a dumb composition but I’ll risk saying that it’s one of the most used. Due to it being commonly used to showing/hiding something in your component when you click a button.

Here’s an example:

const MyComponent = {
    setup() {
        const { value: isModalVisible, toggle: toggleModalVisibility } = useToggle()

        return { isModalVisible, toggleModalVisibility }
    },

    template: ' // I did not use backticks here due to WordPress formatting
        <div>
            <button @click="toggleModalVisibility">Open modal</button>
            <some-modal v-if="isModalVisible" @close="toggleModalVisibility" />
        </div>
    '
}

JSfiddle: https://jsfiddle.net/nolleto/4n3js2qL/3/

useWindowSize

import { ref, onMounted, onUnmounted } from '@vue/composition-api';

const getWindowWidth = () => window.innerWidth
const getWindowHeight = () => window.innerHeight

const useWindowSize = () => {
    const width = ref(getWindowWidth())
    const height = ref(getWindowHeight())

    const onResize = () => {
        width.value = getWindowWidth()
        height.value = getWindowHeight()
    };

    onMounted(() => {
        window.addEventListener('resize', onResize)
    });

    onUnmounted(() => {
        window.removeEventListener('resize', onResize)
    });

    return { width, height }
};

export default useWindowSize

A composition to track the currrent window size.

Here’s an example:

const MyComponent = {
    setup() {
        const { width: windowWidth, height: windowHeight } = useWindowSize()

        return { windowWidth, windowHeight }
    },

    template: `
        <div>
        The window size:
        <p>Width: {{ windowWidth }}</p>
        <p>Height: {{ windowHeight }}</p>
        </div>
    `
}

JSfiddle: https://jsfiddle.net/nolleto/17bzjer4/2/

useStoreFetchAll

import { createNamespacedHelpers } from 'vuex-composition-helpers';

const useStoreFetchAll = (storeNamespace) => {
    const { useState, useGetters, useActions } = createNamespacedHelpers(storeNamespace)
    const { isFetchingAll: isFetching } = useState(['isFetchingAll'])
    const { entries } = useGetters(['entries'])
    const { fetchAll } = useActions(['fetchAll'])

    const loadEntries = async (requestArgs) => {
        const currentEntries = await fetchAll(requestArgs)

        return currentEntries;
    };

    return {
        isFetching,
        entries,
        loadEntries,
    };
};

export default useStoreFetchAll;

vuex-composition-helpers is a package that helps you integrate the store (vuex) with the Composition Api.

If you’re using vuex to execute requests to store the response, this composition may be helpful. Since this is the flow in my project, this composition was created to help us.
As an example, if you need to request the users, you can:
1. Create the users structure in the store (vuex)
2. Create the users composition

Here is the users composition:

import useStoreFetchAll from '@/compositions/use_store_fetch_all';

const useUsers = () => {
    const {
        entries: users,
        isFetching,
        loadEntries: loadUsers,
    } = useStoreFetchAll('users'); // store namespace

    return { users, isFetching, loadUsers }
};

export default useUsers

You may think you can use mapGetters, mapActions, mapMutations and so on to access the data in your component, and you’re right! But, it’s too verbose and on each component you’ll have to declare mapGetters and mapActions. Also, you have to remember the store namespace and the properties as well.

For example:

import { mapGetters, mapActions } from 'vuex'

const MyComponent = {
    computed: {
        ...mapGetter('users', ['users?', 'items?']) // without checking the store it's hard to know which is the right property name
    },

    methods: {
        ...mapActions('users', ['fetch?', 'getAll?'])
    }
}

But with a composition it’s way easier and even your editor (like VS Code) will help you :). Even if you editor doesn’t have auto-complete, you can simply open the composition and check the return line to see all properties that are returned.

import { useUsers } from '@/my-compositions'

const MyComponent = {
    setup(): {
        const { users, isFetching, loadUsers } = useUsers()

        return { users, isFetching, loadUsers }
    }
}

This way you’re centralizing your users fetch logic in two compositions (useStoreFetchAll and useUsers) that can be easily tested, reused and read since it’s a function that stands on it’s own.
And if you want to change something in the request behavior you just need to update useStoreFetchAll composition and all your request will be updated everywhere it’s used.

Conclusion

I really like the Composition API because it let’s you abstract your components as much as possible. This way, your component will have UI logic and the composition will hold behavior the logic.

In the recents projects I’ve been working on, my team and I always abstract the components logics in a composition (or a hook if you are using React) and it’s something that really helps you to break down your code and separate logic in a SOLID way. For example, can you think about that component that you want to reuse but there is too many behaviors on it that in the end you end up creating a new component and re-writing the logic again? Well, with this strategy you will never do that again.

Maybe you need to create a new component but, since you will have more UI components (because they hold no logic, just UI) and the logic in compositions, you can build up a new component just using the UI component + Composition.

Well, I hope you enjoyed this article as much I enjoyed writting it. If you have any thoughts you wish to share or a different opinion don’t forget to leave a commnet so we can discuss about it.

Bye 😀

We are hiring new talents. Do you want to work with us? become@codeminer42.com