Composable Avanzati in Vue 3: Pattern Riutilizzabili e Domande da Colloquio 2026

Guida completa ai composable avanzati di Vue 3 con pattern riutilizzabili, gestione asincrona degli errori, dependency injection con provide/inject e validazione dei form. Include domande da colloquio tecnico aggiornate al 2026.

Pattern avanzati di composable in Vue 3 con esempi di codice TypeScript

I composable di Vue 3 rappresentano il meccanismo standard per estrarre e riutilizzare logica reattiva tra componenti. Con l'introduzione degli Alien Signals e della compatibilita con Vapor Mode in Vue 3.6, la padronanza dei pattern di composable influisce direttamente sull'architettura delle applicazioni, sulle performance e sulla manutenibilita del codice nel 2026.

Cos'e un composable in Vue 3?

Un composable e una funzione che sfrutta la Composition API per incapsulare e riutilizzare logica con stato. A differenza dei mixin, i composable forniscono input e output espliciti, inferenza completa dei tipi TypeScript e zero collisioni di nomi. La convenzione prevede il prefisso use per i nomi dei composable (ad esempio, useCounter, useFetch).

Anatomia di un Composable Ben Strutturato

Un composable di livello production segue una struttura prevedibile: accetta la configurazione tramite argomenti, crea lo stato reattivo internamente ed espone un oggetto di ritorno tipizzato. Questo pattern garantisce componibilita, testabilita e confini API chiari.

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

Il tipo di ritorno esplicito UseCounterReturn svolge due funzioni: documenta l'API pubblica del composable e impedisce l'esposizione accidentale di dettagli implementativi interni. I componenti consumatori destrutturano esattamente cio di cui hanno bisogno, mantenendo trasparenti i binding nel template.

Diversi aspetti di questo pattern meritano attenzione. L'oggetto di opzioni con valori predefiniti consente una configurazione flessibile senza sovraccaricare la firma della funzione. Ogni invocazione di useCounter() crea un'istanza indipendente di stato, eliminando completamente i problemi di stato condiviso che affliggevano i mixin. Questo pattern "opzioni in ingresso, interfaccia tipizzata in uscita" costituisce la base su cui costruire composable piu complessi.

Composable Asincroni con Gestione degli Errori e Stati di Caricamento

Il data fetching rimane uno dei casi d'uso piu comuni per i composable. Un composable asincrono robusto gestisce lo stato di caricamento, gli errori e la pulizia automatica delle risorse: pattern che i selezionatori esaminano con frequenza durante i colloqui tecnici.

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

Questo composable introduce diversi concetti avanzati. Il tipo di parametro MaybeRefOrGetter<string> accetta stringhe semplici, ref o getter, massimizzando la flessibilita per i chiamanti. L'AbortController previene le race condition quando l'URL cambia rapidamente, annullando le richieste obsolete prima che la risposta precedente arrivi. La pulizia tramite onUnmounted evita memory leak e aggiornamenti di stato su componenti distrutti.

Il watchEffect stabilisce un tracking reattivo automatico: qualsiasi ref o getter acceduto all'interno del callback diventa una dipendenza. Se l'URL e una ref e il suo valore cambia, l'effetto si riesegue automaticamente.

Hook del ciclo di vita nei composable

I composable che invocano onMounted, onUnmounted o altri hook del ciclo di vita devono essere chiamati in modo sincrono all'interno di setup(). Invocare un composable dentro un callback asincrono o un setTimeout fallira silenziosamente nella registrazione dell'hook, poiche non esiste un'istanza di componente attiva in quel momento.

Composizione di Composable: Costruire Astrazioni di Livello Superiore

La vera potenza dei composable emerge quando si compongono tra loro. Questo rispecchia il principio di composizione funzionale: unita piccole e focalizzate combinate in comportamenti complessi senza gerarchie di ereditarieta.

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 composizione opera su piu livelli. useDebouncedRef restituisce una ref con debounce integrato che evita richieste eccessive durante la digitazione. useFetchData riceve apiUrl come computed, il che significa che qualsiasi modifica a query o page ricalcola l'URL e scatena automaticamente una nuova richiesta. Il watcher su query reimposta la paginazione alla pagina 1 ogni volta che il termine di ricerca cambia.

Questo pattern dimostra un principio fondamentale: ogni composable risolve un problema specifico e la composizione li connette senza accoppiamento. In un colloquio tecnico, spiegare questa catena di reattivita (query cambia -> apiUrl si ricalcola -> useFetchData si riesegue -> results si aggiorna) dimostra una comprensione profonda del sistema reattivo di Vue.

Pronto a superare i tuoi colloqui su Vue.js / Nuxt.js?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Dependency Injection con provide/inject per Architetture a Composable

Alcuni stati necessitano di essere condivisi attraverso un albero di componenti senza ricorrere al prop drilling. Il sistema provide/inject di Vue, combinato con i composable, crea un pattern di contesto tipizzato e sicuro.

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
}

Questo pattern separa la fornitura dal consumo. Il componente radice (o un layout) invoca provideTheme() una sola volta, e qualsiasi componente discendente accede al contesto tramite useTheme() indipendentemente dalla profondita dell'albero. L'uso di InjectionKey<ThemeContext> con Symbol garantisce la sicurezza dei tipi in fase di compilazione.

La funzione readonly() avvolge le ref esposte per impedire mutazioni dirette dai componenti consumatori. Solo setTheme() puo modificare il tema, imponendo un flusso di dati unidirezionale. La proprieta resolvedTheme risolve il valore 'system' consultando la media query del browser, fornendo sempre un valore concreto ('light' o 'dark') che i componenti possono utilizzare direttamente per applicare gli stili.

Nei colloqui tecnici, questo pattern viene spesso confrontato con React Context o gli store di Pinia. La differenza chiave e che provide/inject opera a livello dell'albero dei componenti (non e globale) e non richiede dipendenze esterne.

Composable Riutilizzabile per la Validazione dei Form

La validazione dei form e un dominio in cui i composable eccellono particolarmente, sostituendo logica ripetitiva con un pattern dichiarativo basato su regole.

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

Il composable riceve valori iniziali e una mappa di regole di validazione per campo. Ogni regola e una funzione che restituisce true se il valore e valido o una stringa con il messaggio di errore. Il metodo validate() itera sulle regole e si ferma al primo errore per ciascun campo, evitando di mostrare piu messaggi simultanei che risulterebbero confusionari per l'utente.

L'uso di reactive() al posto di ref() per i campi e gli errori semplifica l'accesso nei template: fields.email invece di fields.value.email. La proprieta computata isValid si aggiorna automaticamente quando cambia qualsiasi errore, consentendo di abilitare o disabilitare i pulsanti di invio in modo reattivo.

Questo pattern e estensibile. Si possono aggiungere regole asincrone (verifica di disponibilita di un'email), validazione incrociata tra campi (conferma della password) e messaggi di errore internazionalizzati senza modificare la struttura di base del composable.

Testing dei Composable in Isolamento

I composable che utilizzano le API reattive di Vue richiedono un contesto di componente per funzionare correttamente. La funzione helper withSetup risolve questa esigenza creando un componente minimale che esegue il composable all'interno di 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 funzione withSetup e un pattern standard nell'ecosistema Vue per testare composable in isolamento. Monta un componente wrapper che invoca il composable all'interno di setup(), fornendo accesso al risultato e una funzione unmount() per simulare la distruzione del componente.

Ogni test valida un aspetto specifico: inizializzazione con valori predefiniti, rispetto dei limiti configurati e reattivita dei valori computati. Il terzo test e particolarmente importante poiche verifica che doubled si aggiorna automaticamente quando count cambia tramite increment(), confermando il corretto funzionamento del grafo reattivo.

Per composable asincroni come useFetchData, i test richiedono mock di fetch e gestione delle promise con flushPromises() di Vue Test Utils. Nei colloqui tecnici, dimostrare familiarita con questo pattern di testing comunica maturita professionale e impegno verso la qualita del codice.

VueUse come architettura di riferimento

VueUse contiene oltre 200 composable di produzione che coprono API del browser, sensori, animazioni e utility. Lo studio del suo codice sorgente rivela pattern coerenti: oggetti di opzioni per la configurazione, protezioni per la compatibilita SSR e tryOnScopeDispose per la pulizia delle risorse. Rappresenta un riferimento eccellente per le decisioni architetturali relative ai composable.

Domande Frequenti da Colloquio sui Composable di Vue 3

Le seguenti domande compaiono regolarmente nei processi di selezione per posizioni Vue di livello intermedio e senior nel 2026.

In che modo i composable differiscono dai mixin, e perche i mixin sono stati deprecati?

I mixin soffrono di tre problemi strutturali: la fusione implicita delle proprieta causa collisioni di nomi, l'origine di dati e metodi diventa opaca nei componenti che utilizzano piu mixin, e TypeScript non riesce a inferire i tipi attraverso la composizione dei mixin. I composable risolvono tutti e tre i problemi restituendo oggetti espliciti, rendendo il flusso dei dati tracciabile e fornendo inferenza completa dei tipi attraverso firme di funzioni standard.

Quando un composable dovrebbe utilizzare shallowRef invece di ref?

Si utilizza shallowRef quando la ref contiene un oggetto o array di grandi dimensioni che viene sostituito interamente piuttosto che mutato. Poiche shallowRef traccia solo la riassegnazione (non le modifiche alle proprieta interne), evita il sovraccarico del proxying reattivo profondo. Vue 3.6 utilizza per default la reattivita shallow in determinati contesti proprio per questo motivo prestazionale.

Cosa accade se un composable invoca onMounted all'interno di una funzione asincrona?

La registrazione dell'hook del ciclo di vita fallisce silenziosamente. Vue associa gli hook del ciclo di vita all'istanza del componente attualmente attiva, che viene impostata durante l'esecuzione sincrona di setup(). Nel momento in cui un callback asincrono viene eseguito, l'istanza attiva potrebbe essere null o un componente diverso. La soluzione consiste nell'invocare gli hook del ciclo di vita in modo sincrono al livello superiore del composable e utilizzare watchEffect o watch per la logica asincrona.

Come interagiscono i composable con il Vapor Mode di Vue nella versione 3.6?

Vapor Mode compila i template in operazioni DOM dirette, bypassando il Virtual DOM. I composable funzionano in modo identico in Vapor Mode poiche operano a livello del layer di reattivita, non del layer di rendering. Le ref reattive e le proprieta computate dei composable attivano aggiornamenti DOM granulari in modo piu efficiente sotto Vapor Mode, rendendo i composable ben strutturati un vantaggio prestazionale.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

I composable di Vue 3 rappresentano piu di una semplice alternativa ai mixin: costituiscono un paradigma di organizzazione del codice che scala da funzioni utilitarie semplici a sistemi complessi con dependency injection e composizione multilivello. La padronanza di questi pattern distingue gli sviluppatori che si limitano a utilizzare Vue da coloro che ne comprendono il sistema reattivo in profondita.

I punti chiave da consolidare:

  • Struttura chiara: interfacce tipizzate di input e output con UseXxxOptions e UseXxxReturn documentano l'API e impediscono esposizioni accidentali
  • Isolamento dello stato: ogni invocazione crea un'istanza indipendente, eliminando i conflitti tra consumatori
  • Composizione esplicita: composable che consumano altri composable creano catene di reattivita prevedibili e manutenibili
  • Gestione delle risorse: AbortController e onUnmounted garantiscono la pulizia degli effetti collaterali e prevengono memory leak
  • Injection tipizzata: provide/inject con InjectionKey sostituisce il prop drilling senza sacrificare la sicurezza dei tipi
  • Testabilita: il pattern withSetup consente di validare la reattivita in isolamento completo rispetto all'interfaccia utente
  • Pragmatismo: verificare VueUse prima di reimplementare funzionalita comuni risparmia tempo e riduce il rischio di bug

Preparare questi pattern con implementazioni concrete e la capacita di spiegare le decisioni progettuali alla base di ciascuno fornisce un vantaggio significativo nei processi di selezione tecnica per posizioni Vue nel 2026.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#vue
#composables
#typescript
#interview

Condividi

Articoli correlati