Back to code
Vue 3TypeScript
Extract the composable before the component grows a second job
A component that fetches, filters, and renders is doing three jobs. Only one of them is a component's job.
Bad - fetch, state, and markup fused together
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
const invoices = ref<Invoice[]>([]);
const loading = ref(false);
const query = ref("");
onMounted(async () => {
loading.value = true;
const res = await fetch("/api/invoices");
invoices.value = await res.json();
loading.value = false;
});
const filtered = computed(() =>
invoices.value.filter((i) => i.client.includes(query.value))
);
</script>
Nothing here is reusable, and nothing here is testable without mounting the component. The fetch logic and the filter logic can't be reasoned about, or reused, independent of the template.
Good - the component only renders
// composables/useInvoices.ts
export function useInvoices() {
const invoices = ref<Invoice[]>([]);
const loading = ref(false);
async function load() {
loading.value = true;
invoices.value = await api.get<Invoice[]>("/invoices");
loading.value = false;
}
onMounted(load);
return { invoices, loading, reload: load };
}
<script setup lang="ts">
const { invoices, loading } = useInvoices();
const query = ref("");
const filtered = computed(() =>
invoices.value.filter((i) => i.client.includes(query.value))
);
</script>
useInvoices is unit-testable without a DOM, reusable in a second view, and
the component is left doing exactly one job: turning state into markup.
The tradeoff: an extra file and an extra layer of indirection for what used to be inline. Not worth it for a one-off page; worth it the moment two components need the same data.