Vue 3: Composition API in Practice — organizando la lógica real en componentes Vue

Aprende a organizar componentes Vue 3 con Composition API, script setup y composables reutilizables para reducir complejidad.

Avatar de Gabriel
Gabriel
Vue 3: Composition API in Practice — organizando la lógica real en componentes Vue

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/watchEffect y setup() (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, computed y watch.
  • 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)?

Diagrama que compara organizar un componente Vue por opciones (data/methods) frente a organizarlo por funcionalidades (búsqueda/filtros/paginación).
Diagrama que compara organizar un componente Vue por opciones (data/methods) frente a organizarlo por funcionalidades (búsqueda/filtros/paginación).

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:

  1. 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
   }
 }

})

  1. <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>

  1. 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 .value en el código TypeScript.
  • Colocar .value dentro 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:

  1. Entrada: props, emit/context.
  2. Estado: ref, reactive.
  3. Lógica derivada: computed.
  4. Efectos: watch, watchEffect, hooks del ciclo de vida.
  5. 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:

  1. Imports.
  2. defineProps / defineEmits.
  3. Estado reactivo (ref / reactive).
  4. computed.
  5. watch/watchEffect/hooks.
  6. Funciones de acción (por ejemplo, fetchItems, goToPage).
  7. 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)

Representar a ideia de um composable como um “tijolo” reutilizável. - Prompt: `Ilustração abstrata de blocos modulares representando funções reutilizáveis, ícones de código, setas mostrando reuso em m
Representar a ideia de um composable como um “tijolo” reutilizável. - Prompt: `Ilustração abstrata de blocos modulares representando funções reutilizáveis, ícones de código, setas mostrando reuso em m

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, sin ref/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 fetchFn y buildParams: 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

Dashboard con tabla de datos, barra de búsqueda, filtros por estado y controles de paginación, con un estilo de interfaz moderno.
Dashboard con tabla de datos, barra de búsqueda, filtros por estado y controles de paginación, con un estilo de interfaz moderno.

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, watch y created.

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:

  • usePagination
  • useAsyncData

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 }}
<nav> <button :disabled="currentPage === 1" @click="goToPage(currentPage - 1)" > Anterior </button> {{ currentPage }} / {{ totalPages }} <button :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)" > Siguiente </button> </nav>

</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.
  • usePagination y useAsyncData pueden 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:

  1. Elige un componente “problemático” de tu proyecto actual
  2. Preferiblemente, algo como una pantalla de dashboard con búsqueda y filtros.
  3. Reescríbelo internamente con <script setup lang="ts">, sin modificar la interfaz.

  4. Identifica responsabilidades claras

  5. Búsqueda, filtros, paginación, loading/errores, etc.
  6. Agrupa la lógica relacionada (estado + computed + watchers + llamadas a la API).

  7. Extrae un primer composable

  8. Empieza por algo pequeño: usePagination o useAsyncData.
  9. Úsalo en al menos dos componentes para validar el diseño y los tipos.

  10. Refina el estilo de tu equipo

  11. Acuerden convenciones: nombres de composables (useAlgo), estructura interna de <script setup>, cuándo usar ref frente a reactive, cómo tipar fetchFn, etc.

  12. Conecta con la comunidad

  13. Comparte tus casos reales, dudas y patrones descubiertos:

Te gusto el articulo?

Compartelo con tus amigos y ayuda a difundir conocimiento!