Is Returning Composables from Composables an Anti-Pattern in Vue Applications?

  You block advertising 😢
Would you like to buy me a ☕️ instead?

Disclaimer: This article might not meet your expectations if you’re looking for a black-and-white answer to whether returning composables from other composables in Vue is an anti-pattern. Instead, I aim to explore this concept and share my perspective.

Returning a Composable from a Composable

Consider this scenario: we have a useProduct() composable that returns a useList() composable.

// composables/product.ts
export const useProduct = () => {
  const { invoke, useGet } = useApi();

  return {
    useList() {
      return useGet('/api/products');
    },
    async remove() {
      await invoke('/api/products', {
        method: 'DELETE',
      });
    },
  };
};

And its usage in a Vue component:

<script setup lang="ts">
  import { useProduct } from '../composables/product.ts';

  const { useList, remove } = useProduct();
  const { data: products } = await useList();
</script>

<template>
  <ul>
    <li v-for="product in products">
      <!-- ... -->
      <button @click="remove(product.id)">Remove</button>
    </li>
  </ul>
</template>

Situations in Which It Might Be Useful to Return a Composable from Another Composable

You might wonder: why not simply do the following?

// composables/product.ts
export const useList = () => {
  const { useGet } = useApi();

  return useGet('/api/products');
};

// ...

It’s mostly about cosmetics and ergonomics. This approach looks straightforward, but the challenge arises when exposing the remove() function. Consider this:

// composables/product.ts
// ...

export const useRemove = () => {
  const { invoke } = useApi();

  return async () => {
    await invoke('/api/products', {
      method: 'DELETE',
    });
  };
};

And its usage:

<script setup lang="ts">
  import { useList, useRemove } from '../composables/product.ts';

  const { data: products } = await useList();
  const { invoke: remove } = await useRemove();
</script>

<template>
  <ul>
    <li v-for="product in products">
      <!-- ... -->
      <button @click="remove(product.id)">Remove</button>
    </li>
  </ul>
</template>

Though it is not entirely out of line, I find const { invoke: remove } = await useRemove() is somewhat awkward. useRemove() must be a composable because it relies on another composable (useApi()). But it only returns a single function. It’s not a composable in the classical sense which implies that it returns reactive data which we manipulate with functions it supplies.

Anti-Pattern or Not?

Both methods have their quirks.

Pros and Cons of Returning a Composable from Another Composable

Pros:

  • Enhanced ergonomics for users.
  • Encapsulation of related functionalities.

Cons:

  • Deviates from common Vue composables patterns.

Pros and Cons of Using Separate Composables

Pros:

  • Appears less out of the ordinary at first glance.
  • Follows a more traditional Vue composable structure.

Cons:

  • A composable returning a single function straying from the typical pattern of handling reactive data.

Wrapping It Up

In conclusion, while returning a composable from another composable might seem unorthodox, it is tempting to do so in certain situations, mainly because it is slightly more ergonomic. However, it also introduces deviations from established patterns. As with many architectural decisions in software development, the best approach depends on your project’s specific requirements and context.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi