Skip to content

Custom Composable Methods with Vue 3

Introduction

One of the greatest strenghts of modern Javascript frameworks is the ability to reuse components. Components (especially Vue's single-file components) allow you to build a reusable piece of code that handles the template, styling, and logic for a part of your application. Building frontend applications with components is fairly ubiquitous at this point, with the most popular frameworks all adopting this style of building an application.

However, there are times that you need to share code between components. Either it's a utility to help with form validation or an API request that needs to be made from different parts of your application. Maybe it's a timer, where the rendering of the countdown is different depending on what page you're on. Whatever the case may be, you will find yourself in a situation where building a component ends up duplicating code rather than reducing it.

Until Vue 3 and the Composition API, we had two methods of handling this - utility functions imported for a single use or implementing mixins for our Vue components. Utility functions can do a lot, but they are typically framework agnostic and do not handle state or other Vue features. Mixins can cover a large number of cases, but they cause a lot of issues on their own:

  • Multiple mixins could use the same key names, causing values to be different than expected.
  • It's not obvious what methods and attributes are available within a component, leading to confusion about what is and is not present on this.
  • While they help with reusing logic in multiple components, we can't pass parameters into a mixin to customize it for our needs. This leads mixins to be a bit too rigid in practice, and end up being less reusable than we'd like.

In this article, we will explore new options for creating reusable code - custom methods using the Vue 3 Composition API.

If you haven't read my previous articles on the Composition API, I highly recommend reading them before we continue. We won't be using all of these concepts in this article, but it's helpful to keep in mind everything you can do with the Composition API.

Example - Weight Conversion

Let's start with an example of a mixin where we convert a weight to other weight scales. It allows us to configure whether the weight is pounds or kilograms, and uses computed properties to generate the correct measure for each type we want to support. For this example, we will handle pounds, kilograms, metric tons, and short tons.

Here's what that mixin might look like:

import { defineComponent } from "vue";

export default defineComponent({
  data() {
    return {
      weight: 0,
      weightType: "LBS",
    };
  },
  computed: {
    lbs() {
      return this.weightType === "LBS"
        ? this.weight
        : this.weight * 2.20462262185;
    },
    kg() {
      return this.weightType === "LBS" ? this.weight : this.lbs * 0.45359237;
    },
    mt() {
      return this.kg * 0.001;
    },
    st() {
      return this.lbs / 2000;
    },
  },
});

Great! Now in our template we can bind to weight and weightType and utilize the various weights as needed. There are a couple problems with this mixin, however. Because of its nature, it won't be obvious within the component which weights are supported. What if another developer is expecting imperial tons? Or grams? By using this mixin, the developer would need to examine the mixin in order to know for certain what was available.

Also, there is a risk that any of these values could be overridden within the component. If this.lbs is defined anywhere else, it will probably cause errors with the other computed properties. This is due to the mixin not being encapsulated from the rest of the component.

Let's address both of these problems by converting this mixin to a custom Composition API method. We'll still have the benefits of using computed properties and triggering Vue's reactivity system, but the code will be more explicit to future developers.

import { ref, computed } from "vue";

export default function useWeights(
  initialWeight: number = 0,
  initialWeightType: "LBS" | "KG" = "LBS"
) {
  const weight = ref(initialWeight);
  const weightType = ref(initialWeightType);

  const lbs = computed(() =>
    weightType.value === "LBS" ? weight.value : weight.value * 2.20462262185
  );
  const kg = computed(() =>
    weightType.value === "KG" ? weight.value : lbs.value * 0.45359237
  );
  const mt = computed(() => kg.value * 0.001);
  const st = computed(() => lbs.value / 2000);

  return {
    weight,
    weightType,
    lbs,
    mt,
    st,
    kg,
  };
}

Let's go over what we're doing here:

  1. We have created a function that is the default export called useWeights. This is a common naming scheme in React for similar types of functions (custom hooks) and has been picked up in the Vue community as well.
  2. We're using ref to store two local variables - weight and weightType. Because we are using ref, any updates to them will trigger Vue template rendering and the computed properties will recalculate.
  3. We are using computed to create four computed properties, just like in our mixin.
  4. We then return the refs and computed properties.

Now, in our Vue component, we can import this function and use it to access these variables.

import { defineComponent } from "vue";
import useWeights from "./hooks/useWeights";

export default defineComponent({
  name: "App",
  setup() {
    let { weight, weightType, lbs, kg, mt, st } = useWeights();

    return {
      weight,
      weightType,
      lbs,
      kg,
      mt,
      st,
    };
  },
});

By utilizing our useWeights function, we can instantiate the values needed to perform our weight conversions. All of the logic to handle our conversions is bundled inside of the custom function, which means there's no chance of accidentally changing what lbs is supposed to be. And because we are destructuring the return from useWeights, we have full control over the variable names being returned, and they are explicitly set in our component. No more guessing what values are available!

For those of you familiar with Typescript, you may have noticed that our useWeights function implements a small amount of type checking for its inputs. The weightType, for example, has the implicit type of Ref<"LBS" | "KG">. This means that in our Vue component, we can see exactly what types are expected, and even get proper error messaging. For example, setting weightType.value to an unexpected value of "MT" causes the Typescript error, Type '"MT"' is not assignable to type '"LBS" | "KG"'. This isn't as simple to get if we were using mixins!

Example - API Requests

Let's look at another example of a custom Composition API function - making API requests. This is a great example because we can bundle a lot of logic that we'd otherwise need to handle in our component.

import { ref } from "vue";

export const Status = {
  IDLE: "IDLE",
  RUNNING: "RUNNING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
};

export default async function useDadJoke() {
  async function fetchJoke() {
    status.value = Status.RUNNING;
    try {
      const res = await fetch("https://icanhazdadjoke.com/", {
        method: "GET",
        headers: {
          Accept: "application/json",
        },
      });
      if (!res.ok) {
        status.value = Status.ERROR;
      }
      const json = await res.json();
      status.value = Status.SUCCESS;
      return json;
    } catch (err) {
      status.value = Status.ERROR;

      throw new Error(err);
    }
  }

  async function refetchJoke() {
    joke.value = await fetchJoke();
  }

  let status = ref(Status.IDLE);
  let joke = ref(await fetchJoke());

  return {
    joke,
    status,
    refetchJoke,
  };
}

In this function (useDadJoke), we have two more functions - fetchJoke and refetchJoke. fetchJoke is the main content here, using Fetch to make a request, confirm its status, and then return the result. refetchJoke simply calls fetchJoke and stores it on our local ref, joke.

We then declare the two refs that we need, status and joke, then return everything but fetchJoke. This provides the two refs and the refetch function to our Vue component. We can then use this custom function in our component like this:

<template>
  <div v-if="status === Status.RUNNING">Loading...</div>
  <div v-else>{{ joke.joke }}</div>
  <div>
    <button @click="refetchJoke" :disabled="status === Status.RUNNING">
      Refetch joke
    </button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import useDadJoke, { Status } from "../hooks/useDadJoke";

export default defineComponent({
  async setup() {
    let { joke, status, refetchJoke } = await useDadJoke();

    return {
      joke,
      status,
      refetchJoke,
      Status,
    };
  },
});
</script>

As we can see, all of our logic is now contained within the custom function, and can be reused in other components as well. We could also refactor our useDadJoke function to accept a URL as an argument. We could also leverage common libraries like Axios here, and put our configuration into the custom function. This is a great example of what we can do with custom Composition API functions.

Conclusion

Custom Composition API functions have a lot of potential to clean up application code. They really highlight the benefit of what the Composition API provides - being able to group code by feature, rather than function. By breaking up your application's logic into separate functions that are then imported into components, it will be far easier to utilize that logic across your application.

In addition, keep in mind that the Composition API (and Vue's reactivity system) can be used in other contexts than a Vue single-page application. The same logic that you are using in your frontend could also work on your Node backend! By bundling this logic into functions, it helps prevent code duplication in more than just your Vue app.

One last thing - a large number of libraries are already being created to leverage custom Composition API functions. A great example of this is Vue Use by Anthony Fu, which includes a large number of functions for everyday use. Some of them include:

  • useEventListener
  • useDebounce
  • onClickOutside
  • useOnline
  • useInterval/useTimeout
  • useStorage (localStorage and sessionStorage.)

Other libraries, like Vuex and Pinia, also provide custom functions to integrate their libraries using the Composition API, with more on the way. Just like with the React ecosystem when Hooks came out, the Vue ecosystem is slowly adopting these new changes and finding where they work best. Now is a great time to start working on custom Composition API functions for your own applications!

Until next time!