Monitorare i valori delle props in Vue.js

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.

Torna su