Nelle applicazioni Vue.js, le props rappresentano il meccanismo principale attraverso cui i componenti padre trasmettono dati ai componenti figlio. Quando la complessità dell'applicazione cresce, diventa fondamentale non solo ricevere questi valori, ma anche reagire in modo preciso alle loro variazioni nel tempo. Vue.js offre diversi strumenti per osservare e monitorare le props, ciascuno con caratteristiche, vantaggi e casi d'uso specifici.
Questo articolo esplora in profondità le tecniche disponibili per monitorare le props in Vue.js, coprendo sia l'Options API che la Composition API, con esempi pratici e dettagliati per ciascun approccio.
Il ruolo delle props nel flusso di dati unidirezionale
Vue.js adotta un modello di flusso dati unidirezionale: i dati scendono dal componente padre verso i componenti figlio tramite le props. Questo significa che il componente figlio non deve mai modificare direttamente una prop ricevuta; se ha bisogno di un valore locale derivato da essa, deve crearne una copia interna oppure usare una computed property.
Monitorare le props è necessario in numerosi scenari reali: aggiornare uno stato interno quando arriva un nuovo valore, effettuare chiamate API al variare di un parametro, sincronizzare componenti multipli, oppure applicare logica di validazione o trasformazione ogni volta che il valore cambia.
Utilizzo di watch nell'Options API
Nella Options API, la proprietà watch permette di definire osservatori per qualsiasi dato reattivo del componente, incluse le props. La sintassi di base prevede di usare il nome della prop come chiave dell'oggetto watch.
// Componente figlio che osserva una prop
export default {
name: 'UserProfile',
props: {
// Prop che contiene l'ID dell'utente da visualizzare
userId: {
type: Number,
required: true
}
},
data() {
return {
// Dati dell'utente recuperati dal server
userData: null,
// Stato di caricamento
isLoading: false
}
},
watch: {
// Osservatore sulla prop userId
userId(newValue, oldValue) {
console.log(`userId cambiato da ${oldValue} a ${newValue}`)
// Recupera i nuovi dati ogni volta che l'ID cambia
this.fetchUserData(newValue)
}
},
methods: {
async fetchUserData(id) {
this.isLoading = true
// Simulazione di una chiamata API
const response = await fetch(`/api/users/${id}`)
this.userData = await response.json()
this.isLoading = false
}
},
mounted() {
// Caricamento iniziale dei dati al montaggio del componente
this.fetchUserData(this.userId)
}
}
Il callback dell'osservatore riceve due argomenti: il nuovo valore (newValue) e il vecchio valore (oldValue). Questo è utile per confrontare i valori e applicare logica condizionale basata su entrambi.
Opzione immediate
Per impostazione predefinita, un watcher si attiva solo quando il valore osservato cambia dopo il montaggio del componente. Se è necessario eseguire il callback anche al momento della creazione del componente, si usa l'opzione immediate: true. In questo modo, non è necessario duplicare la chiamata nel lifecycle hook created o mounted.
export default {
name: 'ProductDetails',
props: {
// ID del prodotto da visualizzare
productId: {
type: String,
required: true
}
},
data() {
return {
// Dettagli del prodotto
product: null
}
},
watch: {
productId: {
// Il callback viene eseguito immediatamente al montaggio
immediate: true,
handler(newValue) {
this.loadProduct(newValue)
}
}
},
methods: {
async loadProduct(id) {
const response = await fetch(`/api/products/${id}`)
this.product = await response.json()
}
}
}
Opzione deep per osservare oggetti e array
Quando una prop è un oggetto o un array, Vue.js di default non rileva le modifiche alle proprietà interne nidificate: osserva solo il riferimento all'oggetto stesso. Per monitorare ogni mutazione interna è necessario usare l'opzione deep: true.
export default {
name: 'FilterPanel',
props: {
// Oggetto con i filtri di ricerca applicati
filters: {
type: Object,
default: () => ({})
}
},
watch: {
filters: {
// Osserva in profondità tutte le proprietà annidate
deep: true,
immediate: true,
handler(newFilters) {
// Aggiorna i risultati ogni volta che un filtro cambia
this.applyFilters(newFilters)
}
}
},
methods: {
applyFilters(filters) {
// Logica di applicazione dei filtri
console.log('Filtri aggiornati:', filters)
}
}
}
Attenzione: l'opzione deep ha un costo in termini di prestazioni, poiché Vue deve attraversare ricorsivamente l'intera struttura dell'oggetto. In presenza di oggetti molto profondi o frequentemente modificati, è preferibile osservare direttamente la proprietà specifica tramite una stringa con notazione a punto.
Osservare una proprietà specifica con notazione a punto
Vue.js supporta la sintassi a stringa con notazione a punto per osservare una singola proprietà nidificata all'interno di una prop oggetto, evitando l'overhead dell'osservazione profonda.
export default {
name: 'ThemeSelector',
props: {
// Oggetto di configurazione dell'interfaccia
settings: {
type: Object,
required: true
}
},
watch: {
// Osserva solo la proprietà theme dell'oggetto settings
'settings.theme'(newTheme, oldTheme) {
console.log(`Tema cambiato: ${oldTheme} -> ${newTheme}`)
// Applica il nuovo tema al documento
document.documentElement.setAttribute('data-theme', newTheme)
},
// Osserva solo la lingua selezionata
'settings.language'(newLanguage) {
// Aggiorna la localizzazione dell'applicazione
this.updateLocale(newLanguage)
}
},
methods: {
updateLocale(lang) {
console.log('Lingua aggiornata:', lang)
}
}
}
Utilizzo di watch nella Composition API
Nella Composition API, introdotta con Vue 3, la funzione watch importata da Vue offre la stessa flessibilità con una sintassi più esplicita e componibile. Riceve come primo argomento la sorgente reattiva da osservare, che può essere una ref, una reactive property, un getter, o un array di queste.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'SearchResults',
props: {
// Termine di ricerca inserito dall'utente
query: {
type: String,
default: ''
}
},
setup(props) {
// Risultati della ricerca
const results = ref([])
// Stato di caricamento
const isLoading = ref(false)
// Osserva la prop query tramite getter
watch(
() => props.query,
async (newQuery, oldQuery) => {
// Ignora aggiornamenti se la query non è cambiata
if (newQuery === oldQuery) return
isLoading.value = true
const response = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`)
results.value = await response.json()
isLoading.value = false
},
{
// Esegue immediatamente al montaggio del componente
immediate: true
}
)
return { results, isLoading }
}
})
È importante notare che per osservare una prop nella Composition API si usa un getter (() => props.query) e non direttamente props.query. Questo perché le props sono un oggetto reattivo e accedere direttamente al valore della proprietà restituirebbe il valore primitivo corrente, non un riferimento reattivo tracciabile.
Utilizzo di watchEffect
La funzione watchEffect è un'alternativa più automatica a watch: esegue immediatamente il callback e rileva automaticamente le dipendenze reattive usate al suo interno, senza bisogno di dichiararle esplicitamente. È particolarmente utile quando la logica dipende da più props o valori reattivi contemporaneamente.
import { defineComponent, ref, watchEffect } from 'vue'
export default defineComponent({
name: 'DataChart',
props: {
// Dataset da visualizzare nel grafico
dataset: {
type: Array,
required: true
},
// Tipo di grafico da renderizzare
chartType: {
type: String,
default: 'bar'
},
// Colore principale del grafico
primaryColor: {
type: String,
default: '#3498db'
}
},
setup(props) {
// Istanza del grafico
const chartInstance = ref(null)
// Aggiorna il grafico ogni volta che cambia una qualsiasi delle dipendenze
watchEffect(() => {
// Vue rileva automaticamente: props.dataset, props.chartType, props.primaryColor
if (chartInstance.value) {
renderChart({
data: props.dataset,
type: props.chartType,
color: props.primaryColor
})
}
})
function renderChart(config) {
// Logica di rendering del grafico
console.log('Grafico aggiornato con configurazione:', config)
}
return { chartInstance }
}
})
La differenza principale rispetto a watch è che watchEffect non fornisce i valori precedenti e non è possibile limitarlo a una singola sorgente: osserva tutto ciò che viene letto durante l'esecuzione del callback.
Computed properties come alternativa all'osservazione
In molti casi, invece di usare un watcher per derivare un nuovo valore da una prop, è preferibile usare una computed property. Le computed sono memorizzate nella cache, ricalcolate solo quando le dipendenze cambiano, e sono più leggibili e dichiarative rispetto ai watcher per le trasformazioni di dati.
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'PriceDisplay',
props: {
// Prezzo in centesimi
priceInCents: {
type: Number,
required: true
},
// Codice valuta
currency: {
type: String,
default: 'EUR'
},
// Codice lingua per la formattazione
locale: {
type: String,
default: 'it-IT'
}
},
setup(props) {
// Prezzo formattato secondo la valuta e la lingua specificate
const formattedPrice = computed(() => {
return new Intl.NumberFormat(props.locale, {
style: 'currency',
currency: props.currency
}).format(props.priceInCents / 100)
})
// Scaglione di prezzo calcolato automaticamente
const priceCategory = computed(() => {
const price = props.priceInCents / 100
if (price < 10) return 'budget'
if (price < 50) return 'standard'
return 'premium'
})
return { formattedPrice, priceCategory }
}
})
La regola pratica è: se hai bisogno di trasformare o derivare un valore da una prop, usa una computed property. Se hai bisogno di eseguire effetti collaterali (chiamate API, mutazioni del DOM, logging) in risposta a un cambiamento, usa un watcher.
Osservare più props contemporaneamente
Nella Composition API è possibile passare un array di sorgenti a watch, ricevendo nel callback un array dei nuovi valori e un array dei vecchi valori, nell'ordine corrispondente alle sorgenti dichiarate.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'PaginatedList',
props: {
// Categoria degli elementi da visualizzare
category: {
type: String,
required: true
},
// Numero di pagina corrente
page: {
type: Number,
default: 1
},
// Numero di elementi per pagina
pageSize: {
type: Number,
default: 10
}
},
setup(props) {
// Lista degli elementi caricati
const items = ref([])
// Numero totale di elementi disponibili
const totalCount = ref(0)
// Osserva contemporaneamente tutte e tre le props
watch(
[
() => props.category,
() => props.page,
() => props.pageSize
],
async ([newCategory, newPage, newPageSize], [oldCategory]) => {
// Se la categoria è cambiata, torna alla prima pagina
const effectivePage = newCategory !== oldCategory ? 1 : newPage
const response = await fetch(
`/api/items?category=${newCategory}&page=${effectivePage}&size=${newPageSize}`
)
const data = await response.json()
items.value = data.items
totalCount.value = data.total
},
{ immediate: true }
)
return { items, totalCount }
}
})
Pulizia degli effetti nei watcher
Quando un watcher avvia operazioni asincrone come fetch di rete, può capitare che una nuova esecuzione parta prima che la precedente abbia completato, generando condizioni di gara (race conditions). Vue fornisce un meccanismo di pulizia tramite la funzione onCleanup, disponibile come terzo parametro del callback di watch.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'LiveSearch',
props: {
// Termine di ricerca con aggiornamento in tempo reale
searchTerm: {
type: String,
default: ''
}
},
setup(props) {
// Risultati della ricerca
const searchResults = ref([])
// Messaggio di errore eventuale
const errorMessage = ref('')
watch(
() => props.searchTerm,
async (newTerm, oldTerm, onCleanup) => {
// Controller per annullare la richiesta precedente
const controller = new AbortController()
// Registra la funzione di pulizia da eseguire prima della prossima chiamata
onCleanup(() => {
controller.abort()
})
try {
errorMessage.value = ''
const response = await fetch(
`/api/search?q=${encodeURIComponent(newTerm)}`,
{ signal: controller.signal }
)
searchResults.value = await response.json()
} catch (error) {
// Ignora gli errori di abort, sono previsti
if (error.name !== 'AbortError') {
errorMessage.value = 'Errore durante la ricerca'
}
}
},
{ immediate: true }
)
return { searchResults, errorMessage }
}
})
Fermare un watcher manualmente
I watcher creati all'interno di setup() vengono fermati automaticamente quando il componente viene smontato. Tuttavia, in alcuni scenari è utile fermare un watcher prima dello smontaggio. La funzione watch restituisce una funzione stop che può essere chiamata esplicitamente.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'TemporaryMonitor',
props: {
// Valore da monitorare temporaneamente
value: {
type: Number,
required: true
},
// Soglia oltre la quale fermare il monitoraggio
threshold: {
type: Number,
default: 100
}
},
setup(props) {
// Registro degli eventi rilevati
const eventLog = ref([])
// Salva la funzione di stop per usarla in seguito
const stopWatcher = watch(
() => props.value,
(newValue) => {
eventLog.value.push({
time: new Date().toISOString(),
value: newValue
})
// Ferma il monitoraggio quando viene raggiunta la soglia
if (newValue >= props.threshold) {
console.log(`Soglia ${props.threshold} raggiunta. Monitoraggio fermato.`)
stopWatcher()
}
}
)
return { eventLog, stopWatcher }
}
})
Monitoraggio delle props con la sintassi <script setup>
Con la sintassi <script setup> introdotta in Vue 3, il codice diventa ancora più conciso. Le props vengono dichiarate con la macro defineProps e sono accessibili direttamente nell'ambito dello script.
<script setup>
import { ref, watch, computed } from 'vue'
// Dichiarazione delle props con tipo e validazione
const props = defineProps({
// ID dell'ordine da monitorare
orderId: {
type: String,
required: true
},
// Stato attuale dell'ordine
status: {
type: String,
validator: (value) => ['pending', 'processing', 'shipped', 'delivered'].includes(value)
}
})
// Cronologia degli stati dell'ordine
const statusHistory = ref([])
// Notifica da mostrare all'utente
const notification = ref(null)
// Etichette leggibili per ogni stato
const statusLabels = {
pending: 'In attesa',
processing: 'In elaborazione',
shipped: 'Spedito',
delivered: 'Consegnato'
}
// Etichetta formattata dello stato corrente
const currentStatusLabel = computed(() => statusLabels[props.status] ?? props.status)
// Osserva i cambiamenti di stato dell'ordine
watch(
() => props.status,
(newStatus, oldStatus) => {
if (oldStatus) {
// Aggiunge il cambio di stato alla cronologia
statusHistory.value.push({
from: oldStatus,
to: newStatus,
timestamp: new Date()
})
// Mostra notifica all'utente per stati importanti
if (newStatus === 'shipped') {
notification.value = 'Il tuo ordine è stato spedito!'
} else if (newStatus === 'delivered') {
notification.value = 'Il tuo ordine è stato consegnato!'
}
}
}
)
</script>
<template>
<p>Stato: {{ currentStatusLabel }}</p>
<p v-if="notification">{{ notification }}</p>
</template>
Validazione delle props come primo livello di controllo
Prima di usare i watcher per reagire ai valori delle props, è buona pratica definire una validazione robusta delle props stesse. Vue permette di specificare tipo, richiesta obbligatoria, valore di default e una funzione di validazione personalizzata per ciascuna prop.
export default {
name: 'AgeVerifier',
props: {
// Età dell'utente, deve essere un intero positivo
age: {
type: Number,
required: true,
validator(value) {
// Verifica che l'età sia un numero intero positivo ragionevole
return Number.isInteger(value) && value > 0 && value < 150
}
},
// Categoria di accesso richiesta
accessLevel: {
type: String,
default: 'standard',
validator(value) {
// Solo valori predefiniti sono accettati
return ['guest', 'standard', 'premium', 'admin'].includes(value)
}
}
},
computed: {
// Verifica se l'utente è maggiorenne
isAdult() {
return this.age >= 18
},
// Verifica se l'utente ha accesso premium o superiore
hasPremiumAccess() {
return ['premium', 'admin'].includes(this.accessLevel)
}
}
}
Pattern: sincronizzare uno stato interno con una prop
Un caso d'uso molto comune è mantenere uno stato interno del componente sincronizzato con una prop esterna, permettendo al componente di modificare il valore localmente senza alterare direttamente la prop. Questo pattern è utile per input controllati o form con dati pre-popolati.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'EditableField',
props: {
// Valore iniziale fornito dal componente padre
initialValue: {
type: String,
default: ''
}
},
emits: ['update'],
setup(props, { emit }) {
// Copia locale del valore, modificabile dall'utente
const localValue = ref(props.initialValue)
// Indica se ci sono modifiche non salvate
const isDirty = ref(false)
// Sincronizza la copia locale quando la prop esterna cambia
watch(
() => props.initialValue,
(newValue) => {
// Aggiorna solo se non ci sono modifiche locali non salvate
if (!isDirty.value) {
localValue.value = newValue
}
}
)
function handleInput(event) {
localValue.value = event.target.value
isDirty.value = true
}
function saveChanges() {
// Emette il nuovo valore verso il componente padre
emit('update', localValue.value)
isDirty.value = false
}
function discardChanges() {
// Ripristina il valore originale dalla prop
localValue.value = props.initialValue
isDirty.value = false
}
return { localValue, isDirty, handleInput, saveChanges, discardChanges }
}
})
Considerazioni sulle prestazioni
Monitorare le props ha un costo computazionale che varia in base al metodo utilizzato. Alcune linee guida per mantenere elevate le prestazioni dell'applicazione:
Preferire le computed properties ai watcher ogni volta che si tratta di trasformare dati, poiché sono memorizzate nella cache e non eseguono il codice a meno che le dipendenze non cambino realmente. Evitare l'opzione deep: true su oggetti di grandi dimensioni o con strutture molto nidificate: è preferibile osservare specifiche proprietà con la notazione a punto. Usare watchEffect con cautela, poiché traccia automaticamente tutte le dipendenze lette durante l'esecuzione e può rieseguire il callback più spesso del previsto. Applicare tecniche di debouncing o throttling all'interno dei callback dei watcher per le operazioni costose come le chiamate di rete, specialmente quando le props possono cambiare molto rapidamente.
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'DebouncedSearch',
props: {
// Testo di ricerca che può aggiornarsi a ogni tasto premuto
searchText: {
type: String,
default: ''
}
},
setup(props) {
const results = ref([])
// Riferimento al timer di debounce
let debounceTimer = null
watch(
() => props.searchText,
(newText) => {
// Cancella il timer precedente per evitare richieste troppo ravvicinate
clearTimeout(debounceTimer)
// Attende 300ms di inattività prima di eseguire la ricerca
debounceTimer = setTimeout(async () => {
if (newText.trim().length >= 2) {
const response = await fetch(`/api/search?q=${encodeURIComponent(newText)}`)
results.value = await response.json()
} else {
results.value = []
}
}, 300)
}
)
return { results }
}
})
Conclusioni
Vue.js fornisce un sistema flessibile e potente per monitorare i valori delle props, adatto a ogni scenario e stile di sviluppo. La scelta tra watch, watchEffect e le computed properties dipende dalla natura dell'operazione: le trasformazioni dichiarative appartengono alle computed, mentre gli effetti collaterali e le reazioni a cambiamenti specifici appartengono ai watcher.
Padroneggiare questi strumenti significa saper costruire componenti che si comportano correttamente in risposta ai dati ricevuti dall'esterno, senza violare il principio del flusso unidirezionale e mantenendo il codice chiaro, testabile e performante. La Composition API, in particolare, offre maggiore controllo e componibilità rispetto all'Options API, rendendo più semplice gestire casi complessi come la pulizia degli effetti, l'osservazione multipla e la cancellazione manuale dei watcher.