Composables Avanzados en Vue 3: Patrones Reutilizables y Preguntas de Entrevista 2026

Domina los composables avanzados de Vue 3 con patrones reutilizables, manejo de errores asincrono, inyeccion de dependencias y validacion de formularios. Incluye preguntas de entrevista tecnica actualizadas a 2026.

Patrones avanzados de composables en Vue 3 con ejemplos de codigo TypeScript y diagramas de composicion reactiva

La Composition API de Vue 3 transformo la manera en que los desarrolladores organizan y reutilizan logica en aplicaciones frontend. En el centro de esta transformacion se encuentran los composables: funciones que encapsulan estado reactivo, logica de negocio y efectos secundarios en unidades independientes y reutilizables. A diferencia de los mixins de Vue 2 que generaban conflictos de nombres y dependencias ocultas, los composables ofrecen transparencia total, tipado estatico con TypeScript y composicion explicita.

En 2026, el dominio de patrones avanzados de composables se ha convertido en un requisito fundamental para posiciones de desarrollo Vue de nivel intermedio y senior. Esta guia examina los patrones mas solicitados en entrevistas tecnicas, desde la anatomia de un composable bien estructurado hasta la inyeccion de dependencias con provide/inject, incluyendo estrategias de testing que validan la reactividad de cada pieza.

Que es un composable en Vue 3

Un composable es una funcion que utiliza la Composition API de Vue para encapsular y reutilizar logica con estado. Por convencion, los nombres de composables comienzan con use (por ejemplo, useCounter, useFetch). Cada composable retorna refs reactivas, computadas y funciones que los componentes consumen directamente, sin herencia ni acoplamiento implicito.

Anatomia de un Composable Bien Estructurado

Un composable efectivo sigue principios claros: acepta opciones de configuracion mediante un objeto tipado, retorna una interfaz explicita y mantiene su estado interno aislado de otros consumidores. El siguiente ejemplo implementa un contador con limites configurables y un valor computado derivado.

useCounter.tstypescript
import { ref, computed, type Ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
  initialValue?: number
}

interface UseCounterReturn {
  count: Ref<number>
  doubled: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
  const { min = 0, max = Infinity, initialValue = 0 } = options

  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment() {
    if (count.value < max) count.value++
  }

  function decrement() {
    if (count.value > min) count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return { count, doubled, increment, decrement, reset }
}

Varios aspectos de este patron merecen atencion. La interfaz UseCounterReturn define explicitamente el contrato del composable, lo que facilita el autocompletado en editores y previene cambios accidentales en la API publica. El objeto de opciones con valores por defecto permite configuracion flexible sin sobrecargar la firma de la funcion. Cada invocacion de useCounter() crea una instancia independiente de estado, eliminando por completo los problemas de estado compartido que afectaban a los mixins.

Este patron de "opciones de entrada, interfaz tipada de salida" es la base sobre la cual se construyen composables mas complejos.

Composables Asincronos con Manejo de Errores

Las operaciones de red son uno de los casos de uso mas frecuentes para composables. El siguiente patron implementa una funcion de fetching reactiva que cancela peticiones en curso automaticamente, maneja errores de forma granular y responde a cambios en la URL de origen.

useFetchData.tstypescript
import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  isLoading: Ref<boolean>
  refresh: () => Promise<void>
}

export function useFetchData<T>(
  url: MaybeRefOrGetter<string>
): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const isLoading = ref(false)
  let abortController: AbortController | null = null

  async function fetchData() {
    // Cancel any in-flight request
    abortController?.abort()
    abortController = new AbortController()

    isLoading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url), {
        signal: abortController.signal
      })
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      isLoading.value = false
    }
  }

  // Re-fetch when URL changes reactively
  watchEffect(() => {
    fetchData()
  })

  onUnmounted(() => abortController?.abort())

  return { data, error, isLoading, refresh: fetchData }
}

Este composable introduce varios conceptos avanzados. El tipo MaybeRefOrGetter<string> acepta tanto valores estaticos como refs reactivas o funciones getter, otorgando maxima flexibilidad al consumidor. La funcion toValue() extrae el valor subyacente independientemente del tipo de entrada. El uso de AbortController previene race conditions al cancelar peticiones obsoletas cuando la URL cambia antes de que la respuesta anterior llegue.

El watchEffect establece un tracking reactivo automatico: cualquier ref o getter accedido dentro del callback se convierte en una dependencia. Si la URL es una ref y su valor cambia, el efecto se re-ejecuta automaticamente. El hook onUnmounted garantiza la limpieza de recursos cuando el componente se destruye.

Hooks de ciclo de vida en composables

Los hooks como onMounted, onUnmounted y watchEffect solo funcionan correctamente cuando el composable se invoca dentro de setup() o <script setup>. Invocar un composable fuera de este contexto (por ejemplo, en un callback asincrono diferido) resulta en advertencias de runtime y efectos que nunca se registran. Esta restriccion es una de las preguntas mas frecuentes en entrevistas tecnicas de Vue.

Composicion de Composables

La verdadera potencia de los composables emerge cuando se combinan entre si. El siguiente ejemplo construye una busqueda paginada reutilizando useFetchData y un useDebouncedRef hipotetico, demostrando como la composicion produce funcionalidad compleja a partir de piezas simples.

usePaginatedSearch.tstypescript
import { ref, computed, watch, type Ref } from 'vue'
import { useFetchData } from './useFetchData'
import { useDebouncedRef } from './useDebouncedRef'

interface UsePaginatedSearchReturn<T> {
  query: Ref<string>
  page: Ref<number>
  results: Ref<T[] | null>
  totalPages: Ref<number>
  isLoading: Ref<boolean>
  error: Ref<string | null>
  nextPage: () => void
  prevPage: () => void
}

export function usePaginatedSearch<T>(
  baseUrl: string,
  perPage = 20
): UsePaginatedSearchReturn<T> {
  const query = useDebouncedRef('', 300)
  const page = ref(1)
  const totalPages = ref(1)

  const apiUrl = computed(
    () => `${baseUrl}?q=${encodeURIComponent(query.value)}&page=${page.value}&limit=${perPage}`
  )

  const { data, error, isLoading } = useFetchData<{ items: T[]; total: number }>(apiUrl)

  const results = computed(() => data.value?.items ?? null)

  watch(data, (response) => {
    if (response) {
      totalPages.value = Math.ceil(response.total / perPage)
    }
  })

  // Reset to page 1 when query changes
  watch(query, () => { page.value = 1 })

  function nextPage() {
    if (page.value < totalPages.value) page.value++
  }

  function prevPage() {
    if (page.value > 1) page.value--
  }

  return { query, page, results, totalPages, isLoading, error, nextPage, prevPage }
}

La composicion ocurre en multiples niveles. useDebouncedRef retorna una ref con debounce integrado que evita peticiones excesivas durante la escritura. useFetchData recibe apiUrl como computed, lo que significa que cualquier cambio en query o page recalcula la URL y dispara automaticamente una nueva peticion. El watcher sobre query reinicia la paginacion a la pagina 1 cada vez que el termino de busqueda cambia.

Este patron demuestra un principio fundamental: cada composable resuelve un problema especifico y la composicion los conecta sin acoplamiento. En una entrevista tecnica, explicar esta cadena de reactividad (query cambia -> apiUrl se recalcula -> useFetchData re-ejecuta -> results se actualiza) demuestra comprension profunda del sistema reactivo de Vue.

¿Listo para aprobar tus entrevistas de Vue.js / Nuxt.js?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Inyeccion de Dependencias con provide/inject

Algunos estados necesitan compartirse a traves de un arbol de componentes sin prop drilling. El sistema provide/inject de Vue, combinado con composables, crea un patron de contexto tipado y seguro.

useTheme.tstypescript
import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue'

type Theme = 'light' | 'dark' | 'system'

interface ThemeContext {
  theme: Readonly<Ref<Theme>>
  setTheme: (t: Theme) => void
  resolvedTheme: Readonly<Ref<'light' | 'dark'>>
}

const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

export function provideTheme(initial: Theme = 'system') {
  const theme = ref<Theme>(initial)

  const resolvedTheme = computed<'light' | 'dark'>(() => {
    if (theme.value !== 'system') return theme.value
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  })

  function setTheme(t: Theme) {
    theme.value = t
  }

  const context: ThemeContext = {
    theme: readonly(theme),
    setTheme,
    resolvedTheme: readonly(resolvedTheme)
  }

  provide(ThemeKey, context)
  return context
}

export function useTheme(): ThemeContext {
  const context = inject(ThemeKey)
  if (!context) {
    throw new Error('useTheme() requires a parent component to call provideTheme()')
  }
  return context
}

Este patron separa la provision del consumo. El componente raiz (o un layout) invoca provideTheme() una sola vez, y cualquier componente descendiente accede al contexto mediante useTheme() sin importar la profundidad del arbol. El uso de InjectionKey<ThemeContext> con Symbol garantiza seguridad de tipos en tiempo de compilacion.

La funcion readonly() envuelve las refs expuestas para prevenir mutaciones directas desde los componentes consumidores. Solo setTheme() puede modificar el tema, forzando un flujo de datos unidireccional. La propiedad resolvedTheme resuelve el valor 'system' consultando la media query del navegador, proporcionando siempre un valor concreto ('light' o 'dark') que los componentes pueden usar directamente para aplicar estilos.

En entrevistas, este patron suele compararse con React Context o los stores de Pinia. La diferencia clave es que provide/inject opera a nivel del arbol de componentes (no es global) y no requiere dependencias externas.

Composable de Validacion de Formularios

La validacion de formularios es un dominio donde los composables brillan particularmente, reemplazando logica repetitiva con un patron declarativo basado en reglas.

useFormValidation.tstypescript
import { reactive, computed, type UnwrapNestedRefs } from 'vue'

type ValidationRule<T> = (value: T) => string | true
type FieldRules<T> = { [K in keyof T]?: ValidationRule<T[K]>[] }

interface UseFormReturn<T extends Record<string, any>> {
  fields: UnwrapNestedRefs<T>
  errors: Record<keyof T, string>
  isValid: Ref<boolean>
  validate: () => boolean
  resetErrors: () => void
}

export function useFormValidation<T extends Record<string, any>>(
  initialValues: T,
  rules: FieldRules<T>
): UseFormReturn<T> {
  const fields = reactive({ ...initialValues }) as UnwrapNestedRefs<T>

  const errors = reactive(
    Object.keys(initialValues).reduce(
      (acc, key) => ({ ...acc, [key]: '' }),
      {} as Record<keyof T, string>
    )
  )

  function validate(): boolean {
    let valid = true
    for (const key of Object.keys(rules) as (keyof T)[]) {
      const fieldRules = rules[key] || []
      errors[key] = '' as any
      for (const rule of fieldRules) {
        const result = rule(fields[key])
        if (result !== true) {
          errors[key] = result as any
          valid = false
          break // Stop at first error per field
        }
      }
    }
    return valid
  }

  function resetErrors() {
    for (const key of Object.keys(errors)) {
      (errors as any)[key] = ''
    }
  }

  const isValid = computed(() =>
    Object.values(errors).every((e) => e === '')
  )

  return { fields, errors, isValid, validate, resetErrors }
}

El composable recibe valores iniciales y un mapa de reglas de validacion por campo. Cada regla es una funcion que retorna true si el valor es valido o un string con el mensaje de error. El metodo validate() itera sobre las reglas y se detiene en el primer error de cada campo, evitando mostrar multiples mensajes simultaneos que resultan confusos para el usuario.

El uso de reactive() en lugar de ref() para los campos y errores simplifica el acceso en templates: fields.email en lugar de fields.value.email. La propiedad computada isValid se actualiza automaticamente cuando cambia cualquier error, permitiendo habilitar o deshabilitar botones de envio de forma reactiva.

Este patron es extensible. Se pueden agregar reglas asincronas (verificacion de disponibilidad de email), validacion cruzada entre campos (confirmacion de contrasena) y mensajes de error internacionalizados sin modificar la estructura base del composable.

Testing de Composables

Los composables que utilizan APIs reactivas de Vue requieren un contexto de componente para funcionar correctamente. La funcion helper withSetup resuelve esta necesidad creando un componente minimo que ejecuta el composable dentro de setup().

useCounter.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useCounter } from './useCounter'

function withSetup<T>(composable: () => T): { result: T; unmount: () => void } {
  let result!: T
  const wrapper = mount(
    defineComponent({
      setup() {
        result = composable()
        return () => h('div')
      }
    })
  )
  return { result, unmount: () => wrapper.unmount() }
}

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = withSetup(() => useCounter())
    expect(result.count.value).toBe(0)
  })

  it('respects min and max boundaries', () => {
    const { result } = withSetup(() =>
      useCounter({ min: 0, max: 3, initialValue: 3 })
    )
    result.increment()
    expect(result.count.value).toBe(3) // Capped at max

    result.count.value = 0
    result.decrement()
    expect(result.count.value).toBe(0) // Capped at min
  })

  it('computes doubled value reactively', () => {
    const { result } = withSetup(() => useCounter({ initialValue: 5 }))
    expect(result.doubled.value).toBe(10)
    result.increment()
    expect(result.doubled.value).toBe(12)
  })
})

La funcion withSetup es un patron estandar en el ecosistema Vue para testear composables en aislamiento. Monta un componente wrapper que invoca el composable dentro de setup(), proporcionando acceso al resultado y una funcion unmount() para simular la destruccion del componente.

Cada test valida un aspecto especifico: inicializacion con valores por defecto, respeto de limites configurados y reactividad de valores computados. El tercer test es particularmente importante porque verifica que doubled se actualiza automaticamente cuando count cambia mediante increment(), confirmando que el grafo reactivo funciona correctamente.

Para composables asincronos como useFetchData, los tests requieren mocks de fetch y manejo de promesas con flushPromises() de Vue Test Utils. En entrevistas, demostrar familiaridad con este patron de testing comunica madurez profesional y compromiso con la calidad del codigo.

VueUse: biblioteca de composables de referencia

VueUse (vueuse.org) es la biblioteca de composables utilitarios mas utilizada del ecosistema Vue, con mas de 200 funciones que cubren sensores del navegador, animaciones, estado, red y almacenamiento. Antes de construir un composable personalizado, verificar si VueUse ya ofrece una solucion probada y optimizada es una practica recomendada que los entrevistadores valoran.

Preguntas Frecuentes en Entrevistas Tecnicas

Las siguientes preguntas aparecen regularmente en procesos de seleccion para posiciones Vue de nivel intermedio y senior en 2026:

1. Cual es la diferencia entre un composable y un mixin? Los mixins fusionan propiedades en el componente de forma implicita, generando conflictos de nombres y haciendo dificil rastrear el origen de cada propiedad. Los composables retornan valores de forma explicita, ofrecen tipado completo con TypeScript y permiten renombrar las variables al desestructurar el retorno.

2. Por que los composables deben invocarse dentro de setup()? Vue asocia los hooks de ciclo de vida (onMounted, onUnmounted, watch, watchEffect) con la instancia del componente activo durante setup(). Invocar un composable fuera de este contexto rompe esa asociacion, resultando en efectos que nunca se registran ni se limpian.

3. Como se comparte estado entre multiples componentes con composables? Existen dos estrategias principales. Para estado global, se declara la ref fuera de la funcion del composable (patron de singleton). Para estado contextual a un subarbol de componentes, se utiliza provide/inject con InjectionKey tipado, como se demostro en el ejemplo de useTheme.

4. Que es MaybeRefOrGetter y por que es importante? Es un tipo de utilidad de Vue que acepta un valor plano, una Ref o una funcion getter. Permite que los composables acepten entradas tanto estaticas como reactivas, maximizando la flexibilidad para el consumidor. La funcion toValue() extrae el valor subyacente sin importar el tipo de entrada.

5. Como se testean composables que usan onMounted u onUnmounted? Se utiliza una funcion helper como withSetup que monta un componente wrapper. Este componente ejecuta el composable dentro de setup(), proporcionando el contexto de instancia necesario. Para verificar la limpieza, se invoca unmount() y se comprueba que los efectos secundarios (listeners, timers, abort controllers) fueron eliminados.

6. Cuando se debe usar Pinia en lugar de composables con provide/inject? Pinia es preferible cuando el estado necesita ser accesible globalmente desde cualquier componente sin relacion jerarquica, cuando se requiere persistencia (localStorage/sessionStorage), cuando se necesita integracion con Vue DevTools para depuracion, o cuando multiples arboles de componentes necesitan compartir el mismo estado. Los composables con provide/inject son mas apropiados para estado contextual limitado a un subarbol especifico.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Conclusion

Los composables de Vue 3 representan mas que una alternativa a los mixins: son un paradigma de organizacion de codigo que escala desde funciones utilitarias simples hasta sistemas complejos con inyeccion de dependencias y composicion multinivel. El dominio de estos patrones diferencia a los desarrolladores que simplemente usan Vue de aquellos que entienden su sistema reactivo en profundidad.

Los puntos clave para consolidar:

  • Estructura clara: Interfaces tipadas de entrada y salida con UseXxxOptions y UseXxxReturn
  • Aislamiento de estado: Cada invocacion crea una instancia independiente, eliminando conflictos
  • Composicion explicita: Composables que consumen otros composables crean cadenas de reactividad predecibles
  • Manejo de recursos: AbortController y onUnmounted garantizan limpieza de efectos secundarios
  • Inyeccion tipada: provide/inject con InjectionKey reemplaza prop drilling sin sacrificar seguridad de tipos
  • Testabilidad: El patron withSetup permite validar la reactividad en aislamiento completo
  • Pragmatismo: Verificar VueUse antes de reimplementar funcionalidad comun

Preparar estos patrones con implementaciones concretas y la capacidad de explicar las decisiones de diseno detras de cada uno proporciona una ventaja significativa en procesos de seleccion tecnica para posiciones Vue en 2026.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#vue
#composables
#composition-api
#interview
#deep-dive

Compartir

Artículos relacionados