Markus Oberlehner

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


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:

Cons:

Pros and Cons of Using Separate Composables

Pros:

Cons:

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.