Qué es un composable
Un composable es una función que usa la Composition API para encapsular lógica con estado y reutilizarla. No es solo una función auxiliar: puede crear referencias, reaccionar a cambios y limpiar efectos cuando el componente se desmonta.
Objetivo y requisitos
Crearemos useQuery, un composable que recibe una URL fija, una referencia o un getter. Necesitas Vue 3.3 o superior por toValue, TypeScript y un navegador con fetch y AbortController. El servidor debe devolver JSON con la forma que espera tu aplicación.
useQuery.ts completo
Guárdalo en src/composables/useQuery.ts:
import { ref, shallowRef, toValue, watchEffect } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
export function useQuery<T>(url: MaybeRefOrGetter<string>) {
const data = shallowRef<T | null>(null)
const error = ref<string | null>(null)
const isLoading = ref(false)
watchEffect((onCleanup) => {
// La lectura ocurre antes de cualquier await para registrar la dependencia.
const currentUrl = toValue(url)
const controller = new AbortController()
let active = true
onCleanup(() => {
active = false
controller.abort()
})
data.value = null
error.value = null
isLoading.value = false
if (!currentUrl) return
isLoading.value = true
async function load() {
try {
const response = await fetch(currentUrl, { signal: controller.signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const result = (await response.json()) as T
if (active) data.value = result
} catch (cause: unknown) {
if (active && !controller.signal.aborted) {
error.value = cause instanceof Error ? cause.message : 'No se pudo cargar'
}
} finally {
if (active) isLoading.value = false
}
}
void load()
})
return { data, error, isLoading }
}
La limpieza cancela la solicitud anterior y la marca como inactiva. Ese segundo control evita que una respuesta tardía reemplace los datos actuales. Comprobamos response.ok porque fetch no rechaza una promesa solo por recibir un HTTP 404 o 500. El tipo T describe el resultado esperado; no valida la respuesta del servidor en tiempo de ejecución.
Uso completo en App.vue
Para probarlo utilizaremos una API pública de demostración. No envíes información personal a este ejemplo.
<script setup lang="ts">
import { ref } from 'vue'
import { useQuery } from './composables/useQuery'
type Todo = { id: number; title: string; completed: boolean }
const todoId = ref(1)
const { data, error, isLoading } = useQuery<Todo>(
() => `https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
)
</script>
<template>
<label>Tarea
<select v-model.number="todoId">
<option :value="1">Primera</option>
<option :value="2">Segunda</option>
<option :value="99999">Inexistente</option>
</select>
</label>
<p v-if="isLoading" role="status">Cargando…</p>
<p v-else-if="error" role="alert">{{ error }}</p>
<p v-else-if="data">{{ data.id }}: {{ data.title }}</p>
</template>
Comprueba el resultado
Al iniciar se carga la tarea 1. Cambia rápidamente a la 2 con la red ralentizada en las herramientas del navegador: al terminar debe verse la tarea 2. Selecciona la tarea inexistente para comprobar el error HTTP y vuelve a una válida para recuperarte. Al desmontar el componente se cancela la solicitud pendiente. Ejecuta también npm run build.
Errores frecuentes
Pasar todoId.value dentro de una URL ya construida entrega una cadena fija: no cambia cuando cambia la referencia. Usa un getter como en el ejemplo. Llama al composable de forma sincrónica dentro de setup para asociar sus efectos al componente. Cada invocación crea su propio estado; compartir resultados o añadir caché requiere un diseño adicional.
Para un caso con debounce y un servidor de prueba, continúa con watch vs. watchEffect: un cotizador de envíos. Lee también Composables y Watchers en Vue.
Sigue leyendo
Una idea lleva a otra.
)
)
)