Do yourself a favor, don't make it a composable


With the rise of the composition API and the complexity of Vue 3 projects, I see more and more code and business logic placed inside the composables folder, for the sake of reusability and debloating components. This is a good thing to strive for, but I believe many developers are seeing composables as a hammer, and every part of their code as a nail.

Vue documentation mentions a lot of those points, but for some reason many points from this page slip out of mind from time to time. Maybe it is due to how well written those docs are, people are just assuming that the subject is very basic, and they can figure it on their own, without reading the docs first (crazy mental gymnastics, I know, but I have to make any excuse for it). I felt like doing one small page with simple “thumb” rules with explanations, so you can easily spot impostors in your code or during the code review.

Stateless

This one is the most egregious one. Take a look at this snippet:

// DON’T DO THIS
export function useValidator() {
  // this is THE worst email validator I did in my entire life
  const email = (arg: string) => arg.includes('@');
  const firstName = (name: string) => name.length > 3;

  return {
    email,
    firstName,
  };
}

There are a couple of issues with this code:

  1. Every time we call useValidator, JS engine creates a new set of functions email and firstName. It is more efficient to move function declarations to the top level, and return references to those functions instead.
  2. This code has no reactivity and no state whatsoever.

As it is stated in the docs, composable is a way to encapsulate and reuse stateful logic. If your logic consists of simple functions, it is much easier to reuse them as is, without wrapping them into the composable.

export const email = (arg: string) => arg.includes('@');

export const firstName = (name: string) => name.length > 3;

Not only that, but it also makes it much clearer how your code works. Specifically, that it can be reused in any context, as it has no dependency on Vue component instance, no connection to any other composable, store, etc. So you can use it in navigation guard, business logic, store, other composable or even a web worker.

Lifecycle agnostic

If you can call your code outside of setup context like this:


<script setup>
// DON’T DO THIS
const buttonStyle = computed(() => {
  return useButtonStyles().primary;
});
</script>

It means that your code is not a composable. And should not be placed inside the composables folder.

One of the most powerful features of composables is access to the current component instance. This allows composables to use lifecycle hooks and provide/inject API. While the first one is pretty self-explanatory, provide/inject API in composables is a true powerhouse, that lets you use data from plugins like vue-rotuer, pinia, vue-i18n, etc. (this is also a reason why you can’t use them outside of injection context).

Conclusion

Here is a small checklist to understand whether your code is composable or not:

  1. It exposes stateful logic like stores, utilities, etc.
  2. It depends on a store, router or any other plugin or composable that needs to be executed during setup.
  3. It uses lifecycle hooks.

If your code checks none of the boxes above – just make a simple file, and export everything you need.