Errori e fix
Vue 3: le props non si aggiornano, destrutturazione e reattività
Destrutturare le props in `<script setup>` può spezzare la reattività nelle versioni precedenti alla 3.5. La correzione è usare `props.x`, `toRefs` o `defineModel` per i `v-model`.
In sintesi
- Prima di Vue 3.5, `const { title } = defineProps()` perdeva la reattività: il valore restava quello iniziale.
- Da Vue 3.5 la destrutturazione è reattiva, ma va verificata la versione dichiarata nel progetto.
- Per un `v-model` usa `defineModel()`: elimina la coppia prop più emit scritta a mano.
Risposta breve: se il valore resta quello iniziale, la destrutturazione ha copiato la prop in una variabile locale. Usa props.title, oppure toRefs(props). Su Vue 3.5+ la destrutturazione è reattiva, ma solo se la versione è davvero quella.
Il meccanismo: la copia perde il collegamento
defineProps restituisce un proxy reattivo. Il template non legge la prop: legge il proxy, e la lettura viene registrata dall'effetto di render. Destrutturare significa leggere una proprietà una volta e conservarne il valore in una costante normale.
Prima della 3.5 il compilatore generava proprio quella costante, quindi nessuna lettura restava tracciata. Dalla 3.5 il compilatore riscrive gli accessi in modo da passare ancora dal proxy, e la reattività sopravvive. La differenza non è filosofica: dipende dalla versione che il bundler ha risolto.
const { title } = defineProps();
{{ title }}
Il template legge la costante locale, non la prop. Se il genitore cambia title, la costante non cambia. Lo stesso vale per un watch che osserva la variabile copiata: non osserva niente, perché non c'è più nessuna sorgente da seguire.
Le tre correzioni
// 1. accesso diretto: sempre reattivo
const props = defineProps();
// template: {{ props.title }}
// 2. toRefs: ref collegate alla prop
import { toRefs } from "vue";
const { title } = toRefs(props);
// template: {{ title }} (ref scompattata automaticamente)
Su Vue 3.5+ la destrutturazione è supportata dal compilatore e mantiene la reattività. Il punto è verificare la versione: npm ls vue mostra quella risolta, che può essere più vecchia di quella scritta nel package.json, per esempio quando un pacchetto di icone blocca una major diversa.
Il caso del v-model
const model = defineModel({ required: true });
defineModel elimina la coppia prop più emit scritta a mano. È disponibile da Vue 3.4; prima di quella versione serve il binding esplicito, cioè :modelValue più @update:modelValue. Non è zucchero: è il punto in cui il contratto fra genitore e figlio diventa una riga leggibile.
Diagnosi: versione risolta e punto di rottura
Due domande, in quest'ordine. Che versione gira davvero? E la reattività si perde sul valore o sul riferimento? Il primo comando risponde alla prima, il secondo lo conferma sul pacchetto installato invece che sul lockfile.
npm ls vue
node -p "require('vue/package.json').version"
Se la versione è 3.5 o superiore e il valore resta vecchio, la copia non è la causa: il genitore sta mutando un oggetto invece di sostituirlo, oppure esiste un data locale con lo stesso nome della prop. In quel caso il template risolve il nome locale e la prop non compare mai.
Quando la correzione comune peggiora le cose
La prima scorciatoia è copiare la prop in un ref locale con un watch che la riallinea. Funziona per il testo e introduce due fonti di verità: c'è un tick di ritardo, e durante la digitazione il campo riscrive il valore del genitore perdendo il cursore. Il difetto originale torna come salto del cursore, che è più difficile da accettare di un titolo fermo.
La seconda è scrivere nella prop per chiudere il giro, magari tramite v-model="props.title". Con gli oggetti la mutazione passa nel genitore per riferimento e l'avviso in sviluppo sparisce solo in parte: lo stato diventa incoerente e il flusso dei dati smette di essere ricostruibile. Emetti un evento, o usa defineModel.
Un limite da mettere in conto: la destrutturazione reattiva è compilata
Dalla 3.5 la reattività della destrutturazione vive nel codice che il compilatore genera, quindi vale dove il compilatore vede defineProps e le variabili derivate. Un valore destrutturato e passato a un modulo esterno, o chiuso dentro una funzione definita altrove, perde il collegamento: fuori da quel file è di nuovo una costante. Con withDefaults e tipi complessi l'inferenza può inoltre cedere, e la reattività si ferma senza un errore.
Come prevenirlo
- Non destrutturare le props se non sai quale versione di Vue gira.
- Per i `v-model` usa
defineModel, non una coppia artigianale di prop ed emit. - Attiva
vue-tscin CI: i tipi delle props emergono prima del runtime. - Blocca la major di Vue nel package.json e verifica la versione risolta in CI.
Dove continuare
Nel catalogo: componenti, navbar responsive e blocchi. Le guide: librerie UI per Vue 3 e TypeScript o JavaScript per la UI; il confronto: checklist accessibilità.
Domande frequenti
Perché il valore resta quello iniziale?
Perché la destrutturazione copia il valore in una variabile locale. Se la versione di Vue non rende reattiva quella copia, il template continua a leggere il primo valore arrivato.
`toRefs` risolve?
Sì: converte le props in ref e mantiene il collegamento con il proxy. È la forma compatibile con tutte le versioni di Vue 3, anche quelle precedenti alla 3.5.
Posso modificare una prop?
No. Vue avvisa in sviluppo e il flusso dei dati si rompe. Emetti un evento, oppure usa `defineModel` per il caso bidirezionale.
Come verifico la versione di Vue?
`npm ls vue` mostra la versione risolta, che può differire da quella nel package.json per effetto delle dipendenze. Conferma poi leggendo il pacchetto installato.
Vale anche per i `ref` dentro un oggetto?
Sì: se sostituisci l'oggetto intero perdi il riferimento. Aggiorna la proprietà, oppure riassegna un nuovo oggetto reattivo con `reactive`.