Is Returning Composables from Composables an Anti-Pattern in Vue Applications?
- #nuxt ,
- #vue ,
- #architecture
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.