Vue 3: Composition API in Practice — organizando la lógica real en componentes Vue
Este artículo es para quienes ya usan Vue, les gusta la Options API, pero han empezado a notar que sus componentes son demasiado grandes y difíciles de mantener.
TL;DR
- Vue 3: Composition API in Practice no es “otro Vue”: es simplemente una forma diferente de organizar la misma lógica.
- En lugar de separar por “cajas” (
data,methods,computed), pasas a organizar el código por funcionalidades (búsqueda, filtros, paginación, loading…). - Los bloques fundamentales son:
ref,reactive,computed,watch/watchEffectysetup()(o<script setup>). - Los composables son funciones que encapsulan una responsabilidad reactiva (por ejemplo,
useSearchableList,usePagination,useAsyncData). - La Composition API destaca en componentes medianos/grandes y en lógica reutilizable; la Options API sigue siendo excelente para componentes simples.
- Empieza poco a poco: elige hoy un componente difícil de mantener y refactorízalo con la Composition API.
Si quieres profundizar, existe un artículo complementario en inglés llamado “Vue 3 Composition API in Practice”, dentro de un contexto más amplio de ingeniería de software, en el sitio de DW Corp: artículo “Vue 3 Composition API in Practice” de DW Corp.
1. ¿Por qué salir del “Vue básico” y mirar la Composition API?
Si ya has creado un dashboard con búsqueda + filtros + paginación + loading + estado de error en Vue, probablemente hayas sentido algo parecido:
- El componente empezó siendo pequeño… y se convirtió en un monstruo de cientos de líneas.
- La lógica está repartida entre
data,methods,computedywatch. - Reutilizar la misma lógica de búsqueda en otra pantalla significa… copiar y pegar (o usar mixins un tanto mágicos).
Cuando la Options API empieza a doler en la práctica
La Options API funciona muy bien cuando:
- El componente tiene una única responsabilidad clara.
- La lógica es lo suficientemente pequeña como para caber en pocas opciones.
Ahora piensa de nuevo en el dashboard con:
- Búsqueda
- Filtros por estado
- Paginación
- Loading y gestión de errores
- Quizás incluso actualización automática (polling)
Terminas con algo así:
data:items,searchTerm,filters,currentPage,isLoading,error, …computed:filteredItems,paginatedItems,hasMorePages, …methods:fetchItems,applyFilters,handleSearch,goToPage,reload, …watch:searchTerm,filters,currentPage…
Es decir: todo mezclado, y resulta difícil mirar el código y entender qué pertenece a cada funcionalidad.
Lo que aprenderás aquí
Usando el ejemplo de un dashboard con búsqueda + filtros + paginación, veremos en la práctica cómo:
- Organizar mejor la lógica dentro de un componente usando la Composition API.
- Extraer partes de esa lógica en composables reutilizables (por ejemplo,
useSearchableList,usePagination,useAsyncData). - Entender cuándo tiene sentido usar la Composition API y cuándo la Options API sigue siendo una buena elección.
2. ¿Qué es la Composition API (sin buzzwords)?

No es “otro Vue”
La Composition API no es un framework nuevo ni un “modo avanzado secreto”. Es una forma alternativa de declarar la misma lógica que ya escribirías con la Options API.
Cambio de mentalidad:
- Options API: separas el código por tipo de elemento (
data,methods,computed,watch…). - Composition API: separas el código por funcionalidad (búsqueda, filtros, paginación, estado de la petición…).
En la práctica, esto significa:
- Todo lo relacionado con la “búsqueda” puede quedar junto (estado + valores computados + watchers + llamadas a la API).
- Todo lo relacionado con la “paginación” queda junto.
- Todo lo relacionado con el “estado de la petición” (loading/error/datos) queda junto.
Dónde aparece la Composition API
Encontrarás la Composition API principalmente en tres lugares:
setup()dentro de los componentes
ts import { defineComponent, ref } from 'vue'
export default defineComponent({ setup(props, context) { const searchTerm = ref('')
// usar ref, reactive, computed, watch...
return {
searchTerm
// lo que el template puede ver
}
}
})
<script setup>en Single File Components (SFC)
vue
<script setup lang="ts">
import { ref } from 'vue'
const searchTerm = ref<string>('')
</script>
<template> <input v-model="searchTerm" /> </template>
- Funciones reutilizables (composables)
ts import { ref, computed } from 'vue'
export interface SearchableItem { name: string [key: string]: unknown }
export function useSearchableList<T extends SearchableItem>( initialItems: T[] = [] ) { const items = ref<T[]>(initialItems) const searchTerm = ref<string>('')
const filteredItems = computed<T[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
return {
items,
searchTerm,
filteredItems
}
}
Esta función puede importarse en cualquier componente para reutilizar la misma lógica de forma clara.
Vue 3: Composition API in Practice frente a Options API
- Options API
- Muy buena para empezar.
- Excelente para componentes simples.
-
La “forma” del componente es predecible (siempre
data,methods, etc.). -
Vue 3: Composition API in Practice
- Resulta más cómoda en componentes medianos/grandes.
- Es más potente para reutilizar lógica.
- Facilita separar el código por dominio/funcionalidad, y no por “tipo de opción”.
Ambas coexisten en Vue 3. La propia documentación oficial de la Composition API incluye una sección de preguntas frecuentes que analiza en detalle sus motivaciones, beneficios y trade-offs.
3. Fundamentos de la Composition API en la práctica
Antes de hablar de composables, repasemos los bloques básicos, ya con TypeScript.
3.1. Reactividad con ref y reactive
ref: valores simples y puntuales
ref crea un valor reactivo “envuelto” en un objeto con la propiedad .value:
ts import { ref } from 'vue'
const searchTerm = ref<string>('') // string reactivo const currentPage = ref<number>(1) // número reactivo const isLoading = ref<boolean>(false) // booleano reactivo
- En el código, lees y escribes usando
.value:
ts searchTerm.value = 'vue' console.log(currentPage.value)
- En el template, no usas
.value:
vue <template> <input v-model="searchTerm" />
Página actual: {{ currentPage }}
</template>Error típico:
- Olvidar
.valueen el código TypeScript. - Colocar
.valuedentro del template.
reactive: objetos y “estado agrupado”
Cuando tienes un estado que naturalmente es un objeto (por ejemplo, filtros combinados o datos de un formulario), reactive suele resultar más natural:
ts import { reactive } from 'vue'
interface Filters { status: 'all' | 'open' | 'closed' minDate: Date | null maxDate: Date | null }
const filters = reactive<Filters>({ status: 'all', minDate: null, maxDate: null })
Accedes a las propiedades normalmente:
ts filters.status = 'open' console.log(filters.minDate)
Pero hay un detalle importante:
Si desestructuras un objeto
reactive, la reactividad se pierde en las variables desestructuradas.
Ejemplo problemático:
ts // ❌ NO hagas esto const { status, minDate } = filters // status y minDate ahora son copias, no son reactivos
Reglas mentales rápidas:
ref- Para valores primitivos (string, number, boolean).
-
Para estados que manipulas de forma aislada (por ejemplo,
currentPage,searchTerm). -
reactive - Para objetos que representan un estado cohesivo (por ejemplo,
filters,form,queryParams). - Evita desestructurar el objeto o asegúrate de saber exactamente lo que estás haciendo.
3.2. Derivar valores con computed
computed genera un valor que depende de otros valores reactivos y se recalcula automáticamente cuando cambian sus dependencias.
En nuestro dashboard:
ts import { ref, computed } from 'vue'
interface Item { id: number name: string status: 'published' | 'draft' }
const items = ref<Item[]>([ { id: 1, name: 'Vue 3 Guide', status: 'published' }, { id: 2, name: 'Composition API Tips', status: 'draft' } ])
const searchTerm = ref<string>('vue')
const filteredItems = computed<Item[]>(() => { const term = searchTerm.value.toLowerCase() return items.value.filter(item => item.name.toLowerCase().includes(term) ) })
En el template:
vue
<template>
<input v-model="searchTerm" placeholder="Buscar..." />
- {{ item.name }}
</template>
Aquí separas claramente:
- Estados de origen (
items,searchTerm). - Lógica derivada (
filteredItems).
3.3. Reaccionar a cambios con watch y watchEffect
A veces necesitas efectos secundarios:
- Llamar a una API cuando cambia un filtro.
- Guardar algo en
localStorage. - Sincronizar la URL con el estado interno.
Cuándo tiene sentido watch
watch observa una o más fuentes reactivas específicas y ejecuta una función cuando cambian:
ts import { ref, watch } from 'vue'
const searchTerm = ref<string>('') const currentPage = ref<number>(1)
watch( [searchTerm, currentPage], ([newSearch, newPage], [oldSearch, oldPage]) => { // tienes acceso al valor nuevo y al anterior fetchItems({ search: newSearch, page: newPage }) } )
async function fetchItems(params: { search: string; page: number }) { // llamada a la API... }
Casos habituales para watch:
- Llamadas a la API como respuesta a cambios en filtros o paginación.
- Lógica manual de debounce/throttle.
- Persistencia del estado (por ejemplo, guardar las preferencias del usuario).
watchEffect frente a watch
watchEffect ejecuta la función inmediatamente y rastrea automáticamente las dependencias reactivas usadas en ella:
ts import { ref, watchEffect } from 'vue'
const searchTerm = ref<string>('') const currentPage = ref<number>(1)
watchEffect(() => { fetchItems({ search: searchTerm.value, page: currentPage.value }) })
Diferencias importantes:
watch:- Declaras explícitamente qué se observa.
-
Es ideal cuando quieres un control detallado (comparar valores anteriores, configurar
flush, etc.). -
watchEffect: - Es más rápido de escribir.
- Resulta útil para efectos simples vinculados a varios estados.
- Puede volverse confuso si la función empieza a acceder a muchos estados (las dependencias quedan “ocultas”).
4. De setup() a <script setup>: organizar el componente
4.1. Anatomía de un componente con Composition API
Imaginemos un componente DashboardList.vue con búsqueda, filtros, paginación, loading y errores.
Con setup(), la estructura mental es:
- Entrada:
props,emit/context. - Estado:
ref,reactive. - Lógica derivada:
computed. - Efectos:
watch,watchEffect, hooks del ciclo de vida. - Retorno: lo que el template puede utilizar.
Esquemáticamente:
ts import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue'
interface Filters { status: string }
interface Item { id: number name: string }
export default defineComponent({ props: { initialStatus: { type: String, default: 'all' } }, setup(props, { emit }) { // 1. estado const searchTerm = ref<string>('') const currentPage = ref<number>(1) const filters = reactive<Filters>({ status: props.initialStatus }) const items = ref<Item[]>([]) const isLoading = ref<boolean>(false) const error = ref<string | null>(null)
// 2. valores derivados
const filteredItems = computed<Item[]>(() => {
// usa items, searchTerm, filters...
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
// 3. efectos
async function fetchItems() {
// llamada a la API usando filtros, paginación...
}
watch(
[searchTerm, () => filters.status, currentPage],
() => {
fetchItems()
}
)
// 4. ciclo de vida
onMounted(() => {
fetchItems()
})
// 5. retorno para el template
return {
searchTerm,
filters,
currentPage,
items,
filteredItems,
isLoading,
error,
fetchItems
}
} })
Errores habituales:
- Declarar un ref o computed y olvidar devolverlo desde
setup(). - Saturar un único bloque
setup()sin separarlo visualmente por responsabilidad.
4.2. Usar <script setup> en el día a día
<script setup> es un “atajo” para la Composition API en los SFC:
- No necesitas declarar
setup()manualmente. - Todo lo que declares en
<script setup>estará disponible automáticamente en el template. - Hay menos “ceremonia” y puedes concentrarte más en la lógica.
Ejemplo equivalente con <script setup> y TypeScript:
vue
<script setup lang="ts"> import { ref, reactive, computed, watch, onMounted } from 'vue' interface Filters { status: string } interface Item { id: number name: string } // props const props = defineProps<{ initialStatus?: string }>() // 1. estado const searchTerm = ref<string>('') const currentPage = ref<number>(1) const filters = reactive<Filters>({ status: props.initialStatus ?? 'all' }) const items = ref<Item[]>([]) const isLoading = ref<boolean>(false) const error = ref<string | null>(null) // 2. valores derivados const filteredItems = computed<Item[]>(() => { const term = searchTerm.value.toLowerCase() return items.value.filter(item => item.name.toLowerCase().includes(term) ) }) // 3. efectos async function fetchItems(): Promise<void> { // llamada a la API } watch( [searchTerm, () => filters.status, currentPage], () => { fetchItems() } ) // 4. ciclo de vida onMounted(() => { fetchItems() }) </script><template>
</template>
Un orden sencillo que funciona bien en la mayoría de los componentes:
- Imports.
defineProps/defineEmits.- Estado reactivo (
ref/reactive). computed.watch/watchEffect/hooks.- Funciones de acción (por ejemplo,
fetchItems,goToPage). - Llamadas iniciales (
onMounted, etc.).
Siempre que tenga sentido, agrupa visualmente por “feature”:
- Bloque de búsqueda
- Bloque de filtros
- Bloque de paginación
- Bloque de estado de la petición
Esto prepara el terreno para el siguiente paso: extraer composables.
5. Composables: extraer y reutilizar lógica de verdad
5.1. Qué es un composable (y qué no es)

Un composable es:
Una función que usa la Composition API (
ref,reactive,computed,watch, hooks del ciclo de vida) para encapsular una responsabilidad reactiva.
Ejemplos de responsabilidades habituales:
- Gestionar la búsqueda y los filtros de una lista (
useSearchableList). - Controlar la paginación (
usePagination). - Centralizar la lógica de llamadas asíncronas con loading y errores (
useAsyncData).
Lo que no es necesariamente un composable:
- Funciones puras de utilidad que solo reciben datos y devuelven un resultado (por ejemplo,
formatCurrency,parseDate). Estas pueden (y deben) seguir siendo helpers normales, sinref/reactive.
Diferencia frente a los mixins:
- Los mixins inyectan propiedades y métodos de forma “mágica” en el componente.
- Los composables son funciones explícitas: los importas, los llamas y recibes un “paquete” de estado y funciones.
5.2. Construir un composable paso a paso (con TypeScript)
Tomemos una parte de la pantalla del dashboard: listado con búsqueda y loading.
1) Lógica inline en el componente
vue
<script setup lang="ts"> import { ref, computed, watch, onMounted } from 'vue' // import { api } from '@/services/api' interface Item { id: number name: string } const searchTerm = ref<string>('') const items = ref<Item[]>([]) const isLoading = ref<boolean>(false) const error = ref<string | null>(null) async function fetchItems(): Promise<void> { isLoading.value = true error.value = null try { // llamada a la API // const response = await api.get<Item[]>('/items', { // params: { search: searchTerm.value } // }) // items.value = response.data // para el ejemplo, lo simulamos: items.value = [ { id: 1, name: 'Vue 3 Guide' }, { id: 2, name: 'Composition API in Practice' } ] } catch (err) { error.value = 'Error al cargar los elementos' } finally { isLoading.value = false } } const filteredItems = computed<Item[]>(() => { const term = searchTerm.value.toLowerCase() return items.value.filter(item => item.name.toLowerCase().includes(term) ) }) watch(searchTerm, () => { fetchItems() }) onMounted(() => { fetchItems() }) </script>Funciona, pero esta combinación de estado + fetch + loading + error probablemente se repetirá.
2) Extraer a useSearchableList()
ts // useSearchableList.ts import { ref, computed, watch, onMounted } from 'vue'
export interface UseSearchableListParams<TSearchParams> { fetchFn: (params: TSearchParams) => Promise<unknown[]> buildParams: (search: string) => TSearchParams immediate?: boolean }
export function useSearchableList<TItem, TSearchParams = { search: string }>({ fetchFn, buildParams, immediate = true }: UseSearchableListParams<TSearchParams>) { const searchTerm = ref<string>('') const items = ref<TItem[]>([]) const isLoading = ref<boolean>(false) const error = ref<string | null>(null)
async function load(): Promise<void> { isLoading.value = true error.value = null
try {
const params = buildParams(searchTerm.value)
const data = await fetchFn(params)
items.value = data as TItem[]
} catch (err) {
error.value =
err instanceof Error ? err.message : 'Error al cargar'
} finally {
isLoading.value = false
}
}
const filteredItems = computed<TItem[]>(() => { const term = searchTerm.value.toLowerCase() return items.value.filter((item: any) => String(item.name ?? '') .toLowerCase() .includes(term) ) })
watch(searchTerm, () => { void load() })
if (immediate) { onMounted(() => { void load() }) }
return { // estado searchTerm, items, filteredItems, isLoading, error, // acciones reload: load } }
Puntos importantes:
- El composable recibe
fetchFnybuildParams: no sabe cómo buscar; solo coordina. - Expone únicamente lo que tiene sentido para el componente: estado y acciones.
3) Usar el composable en un componente
vue
<script setup lang="ts"> import { useSearchableList } from '@/composables/useSearchableList' // import { api } from '@/services/api' interface Item { id: number name: string } const { searchTerm, filteredItems, isLoading, error, reload } = useSearchableList<Item, { search: string }>({ buildParams: (search: string) => ({ search }), fetchFn: async ({ search }) => { // const response = await api.get<Item[]>('/items', { params: { search } }) // return response.data // ejemplo simplificado: return [ { id: 1, name: `Elemento con filtro: ${search}` } ] }, immediate: true }) </script><template> <input v-model="searchTerm" placeholder="Buscar..." /> <button @click="reload">Recargar</button>
Cargando...
{{ error }}
- {{ item.name }}
</template>
Puedes reutilizar el mismo composable en otra pantalla, cambiando únicamente el tipo Item y la función fetchFn.
5.3. Buenas prácticas y trampas en composables

Buenas prácticas:
- Asigna a cada composable una responsabilidad clara:
useSearchableList→ búsqueda en listas.usePagination→ paginación.-
useAsyncData→ estado de peticiones asíncronas. -
No intentes “reflejar” el componente entero dentro de un composable:
-
Los composables son bloques que el componente combina.
-
Evita composables que sepan demasiado sobre la UI:
- Deja el texto, las etiquetas, los colores y el layout para los componentes.
- Los composables se ocupan del estado y las reglas.
Trade-off con TypeScript:
- Los composables son un poco más verbosos (interfaces, genéricos).
- A cambio, TypeScript ayuda a:
- Documentar parámetros y retornos.
- Evitar errores de uso (tipar
fetchFn, estados, etc.).
6. Caso real: organizar un componente “desordenado” con Composition API
6.1. El componente “antes”: Options API con responsabilidades mezcladas
Un DashboardList.vue con Options API podría ser algo así:
ts import { defineComponent } from 'vue' // import { api } from '@/services/api'
interface Item { id: number name: string }
export default defineComponent({ data() { return { items: [] as Item[], searchTerm: '', statusFilter: 'all', currentPage: 1, pageSize: 20, totalItems: 0, isLoading: false, error: null as string | null } }, computed: { filteredItems(): Item[] { // filtro por searchTerm y status return this.items }, paginatedItems(): Item[] { // divide filteredItems según currentPage/pageSize return this.filteredItems } }, methods: { async fetchItems(): Promise<void> { this.isLoading = true this.error = null
try {
// const response = await api.get('/items', {
// params: {
// search: this.searchTerm,
// status: this.statusFilter,
// page: this.currentPage,
// pageSize: this.pageSize
// }
// })
// this.items = response.data.items
// this.totalItems = response.data.total
} catch (err) {
this.error = 'Error al cargar los elementos'
} finally {
this.isLoading = false
}
},
handleSearchChange(): void {
this.currentPage = 1
void this.fetchItems()
},
handleStatusChange(): void {
this.currentPage = 1
void this.fetchItems()
},
goToPage(page: number): void {
this.currentPage = page
void this.fetchItems()
}
}, watch: { searchTerm() { this.handleSearchChange() }, statusFilter() { this.handleStatusChange() } }, created() { void this.fetchItems() } })
Funciona, pero:
- La lógica de búsqueda, filtros, paginación, loading/errores y peticiones está muy acoplada.
- Reutilizar la paginación en otro componente exige copiar buena parte del código.
- Entender el flujo completo requiere recorrer
data,computed,methods,watchycreated.
6.2. Migrar mentalmente a Composition API
Primer paso: todavía sin composables, solo reorganizar por funcionalidad en <script setup>.
vue
<script setup lang="ts"> import { ref, computed, watch, onMounted } from 'vue' // import { api } from '@/services/api' interface Item { id: number name: string } // --- Estado base --- const items = ref<Item[]>([]) const totalItems = ref<number>(0) const isLoading = ref<boolean>(false) const error = ref<string | null>(null) // --- Búsqueda y filtros --- const searchTerm = ref<string>('') const statusFilter = ref<string>('all') // --- Paginación --- const currentPage = ref<number>(1) const pageSize = ref<number>(20) // --- Valores derivados --- const filteredItems = computed<Item[]>(() => { // filtro local por searchTerm/status, si es necesario return items.value }) const paginatedItems = computed<Item[]>(() => { const start = (currentPage.value - 1) * pageSize.value const end = start + pageSize.value return filteredItems.value.slice(start, end) }) // --- Petición --- async function fetchItems(): Promise<void> { isLoading.value = true error.value = null try { // const response = await api.get('/items', { // params: { // search: searchTerm.value, // status: statusFilter.value, // page: currentPage.value, // pageSize: pageSize.value // } // }) // items.value = response.data.items // totalItems.value = response.data.total } catch (err) { error.value = 'Error al cargar los elementos' } finally { isLoading.value = false } } // --- Reacciones --- watch([searchTerm, statusFilter], () => { currentPage.value = 1 void fetchItems() }) watch(currentPage, () => { void fetchItems() }) onMounted(() => { void fetchItems() }) </script>Incluso sin extraer nada, ya resulta más fácil distinguir cada responsabilidad.
6.3. Extraer composables realmente útiles
Ahora separaremos dos composables simples:
usePaginationuseAsyncData
usePagination
ts // usePagination.ts import { ref, computed } from 'vue'
export interface UsePaginationOptions { pageSize?: number }
export function usePagination(options: UsePaginationOptions = {}) { const perPage = ref<number>(options.pageSize ?? 20) const currentPage = ref<number>(1) const totalItems = ref<number>(0)
const totalPages = computed<number>(() => { if (perPage.value === 0) return 0 return Math.ceil(totalItems.value / perPage.value) })
function goToPage(page: number): void { currentPage.value = page }
function resetPage(): void { currentPage.value = 1 }
return { currentPage, perPage, totalItems, totalPages, goToPage, resetPage } }
useAsyncData
ts // useAsyncData.ts import { ref } from 'vue'
export function useAsyncData<TData, TParams = void>( fetchFn: (params: TParams) => Promise<TData> ) { const data = ref<TData | null>(null) const isLoading = ref<boolean>(false) const error = ref<string | null>(null)
async function load(params: TParams): Promise<void> { isLoading.value = true error.value = null
try {
data.value = await fetchFn(params)
} catch (err) {
error.value =
err instanceof Error ? err.message : 'Error al cargar'
} finally {
isLoading.value = false
}
}
return { data, isLoading, error, load } }
Reescribir el componente con composables
vue
<script setup lang="ts"> import { ref, computed, watch, onMounted } from 'vue' import { usePagination } from '@/composables/usePagination' import { useAsyncData } from '@/composables/useAsyncData' // import { api } from '@/services/api' interface Item { id: number name: string } // --- Búsqueda y filtros --- const searchTerm = ref<string>('') const statusFilter = ref<string>('all') // --- Paginación --- const { currentPage, perPage, totalItems, totalPages, goToPage, resetPage } = usePagination({ pageSize: 20 }) // --- Petición asíncrona --- const { data: items, isLoading, error, load: loadItems } = useAsyncData<Item[], { search: string status: string page: number pageSize: number }>(async ({ search, status, page, pageSize }) => { // const response = await api.get('/items', { // params: { search, status, page, pageSize } // }) // totalItems.value = response.data.total // return response.data.items as Item[] // ejemplo simplificado: totalItems.value = 100 return [ { id: 1, name: `Elemento ${search} - página ${page}` } ] }) // --- Valores derivados --- const filteredItems = computed<Item[]>(() => { const term = searchTerm.value.toLowerCase() return (items.value ?? []).filter(item => item.name.toLowerCase().includes(term) ) }) // --- Orquestación --- function reload(): void { void loadItems({ search: searchTerm.value, status: statusFilter.value, page: currentPage.value, pageSize: perPage.value }) } // --- Reacciones --- watch([searchTerm, statusFilter], () => { resetPage() reload() }) watch(currentPage, () => { reload() }) onMounted(() => { reload() }) </script><template>
<input v-model="searchTerm" placeholder="Buscar..." />
<select v-model="statusFilter"> <option value="all">Todos</option> <option value="open">Abiertos</option> <option value="closed">Cerrados</option> </select>
<button @click="reload">Recargar</button>
Cargando...
{{ error }}
- {{ item.name }}
</template>
Resultado:
- El componente se centra más en orquestar la búsqueda, los filtros y la paginación que en los detalles de implementación.
usePaginationyuseAsyncDatapueden reutilizarse en otras pantallas.- Si cambia la regla de paginación, probablemente solo tengas que modificar
usePagination.
Trade-offs:
- Hay más archivos (composables) que mantener.
- El equipo debe alinearse sobre cómo diseñar composables.
- En proyectos medianos/grandes, la mejora en claridad y reutilización suele compensarlo.
7. Cuándo usar (o no) Vue 3: Composition API in Practice
Casos en los que la Composition API destaca
Suele marcar una gran diferencia cuando:
- Tienes componentes medianos/grandes con varias responsabilidades (como dashboards con búsqueda, filtros, paginación, loading, errores…).
- La lógica de negocio es compleja y compartida (reglas de dominio, formularios complejos, integraciones con APIs externas).
- El proyecto está pensado para el mantenimiento en equipo durante bastante tiempo.
Contar con composables bien definidos (usePagination, useAsyncData, useSearchableList) crea un vocabulario común dentro del equipo.
Casos en los que la Options API sigue teniendo sentido
La Options API sigue siendo totalmente válida cuando:
- El componente es simple y aislado:
- Un botón especializado.
- Una tarjeta con uno o dos estados.
- El equipo todavía está aprendiendo Vue y no siente un problema real con la Options API.
- Quieres crear un prototipo rápidamente sin preocuparte demasiado por la estructura futura.
Un enfoque equilibrado:
- Código nuevo y más complejo → empieza con
<script setup>y Composition API. - Componentes pequeños → la Options API sigue estando bien.
- Refactoriza poco a poco solo lo que hoy resulta problemático.
Estrategias de adopción gradual
- Usa
<script setup>en componentes nuevos, incluso sin composables al principio. - Crea tus primeros composables para problemas concretos:
- Patrón de loading/error en llamadas a la API.
- Paginación ya existente en dos pantallas.
- Cuando algo se utilice en más de un componente, considera extraerlo a un composable.
Próximos pasos
Si has llegado hasta aquí, ya tienes una base suficiente para usar Vue 3: Composition API in Practice en tu día a día. Sugerencia de próximos pasos:
- Elige un componente “problemático” de tu proyecto actual
- Preferiblemente, algo como una pantalla de dashboard con búsqueda y filtros.
-
Reescríbelo internamente con
<script setup lang="ts">, sin modificar la interfaz. -
Identifica responsabilidades claras
- Búsqueda, filtros, paginación, loading/errores, etc.
-
Agrupa la lógica relacionada (estado + computed + watchers + llamadas a la API).
-
Extrae un primer composable
- Empieza por algo pequeño:
usePaginationouseAsyncData. -
Úsalo en al menos dos componentes para validar el diseño y los tipos.
-
Refina el estilo de tu equipo
-
Acuerden convenciones: nombres de composables (
useAlgo), estructura interna de<script setup>, cuándo usarreffrente areactive, cómo tiparfetchFn, etc. -
Conecta con la comunidad
- Comparte tus casos reales, dudas y patrones descubiertos:
- Participa en la comunidad SCCB — perfil de Software Craftsmanship Brasília en Instagram
- Consulta los próximos eventos — agenda de eventos de SCCB en Instagram

