modulo 07 / lezione 38

Debugging asincrono e interfacce dinamiche

Eventi, listener e stato dell'interfaccia

Individuare listener duplicati, eventi non rimossi, propagazione inattesa, stato UI non sincronizzato e bug specifici del framework.

50 minDebugging

In parole semplici

Listener mal gestiti causano una galleria di bug peculiari: pulsanti che reagiscono due volte, modali che non si chiudono, scroll handler che esauriscono la CPU, stati che mostrano dati di un componente che non c’è più. Tutti si curano con poche regole sui cicli di vita dei listener.

Parole nuove

  • listener (event listener): funzione registrata per reagire ad un evento.
  • propagazione (bubbling/capture): l’evento sale dal target verso il root o viceversa.
  • stopPropagation / preventDefault: bloccano la propagazione o il comportamento di default.
  • delegazione eventi: un solo listener sul genitore che gestisce eventi figli.

Cosa impari

  • Aggiungere e rimuovere listener in modo simmetrico.
  • Diagnosticare listener duplicati con DevTools.
  • Capire bubbling, capture, stopPropagation.
  • Riconoscere bug specifici del framework (stale closure, reactivity caveats).

Regola d’oro: ogni addEventListener ha il suo removeEventListener

function handler() { console.log("click"); }
btn.addEventListener("click", handler);
// più tardi:
btn.removeEventListener("click", handler);

removeEventListener richiede la stessa referenza di funzione. Listener inline:

btn.addEventListener("click", () => console.log("click"));
// impossibile rimuovere: la funzione è anonima, niente referenza

In framework moderni il framework si occupa del ciclo di vita. Nel codice vanilla, è tua responsabilità.

AbortController (di nuovo)

Modo idiomatico moderno:

const controller = new AbortController();
btn.addEventListener("click", handler1, { signal: controller.signal });
input.addEventListener("input", handler2, { signal: controller.signal });
// più tardi:
controller.abort();  // rimuove tutti i listener associati

Diagnosticare listener duplicati

In DevTools Chrome: tab Elements → pannello Event Listeners sulla destra. Mostra tutti i listener attivi su un nodo. Click destro → “Remove” per provare.

Sintomi di duplicazione:

  • “Cliccando una volta, l’azione succede due volte.”
  • “Modal si apre e si chiude immediatamente.”
  • “L’handler logga lo stesso valore due volte.”

Cause comuni:

  • addEventListener chiamato due volte senza rimozione.
  • Componente che si rimonta senza cleanup.
  • Listener attaccato a document invece che al nodo specifico.

Bubbling e capture

Quando l’utente clicca su <button> dentro <div> dentro <section>, l’evento attraversa 3 fasi:

  1. Capture: dall’html giù verso il target.
  2. Target: sul nodo stesso.
  3. Bubble: dal target su fino all’html.

addEventListener di default ascolta in fase bubble. Per capture:

parent.addEventListener("click", h, true);  // true = capture
parent.addEventListener("click", h, { capture: true });

event.stopPropagation() ferma la propagazione (nelle fasi successive). event.preventDefault() impedisce l’azione di default del browser (es. submit di form, link).

Delegazione eventi

Anziché 100 listener su 100 bottoni, uno solo sul genitore:

container.addEventListener("click", (e) => {
  const btn = e.target.closest(".azione");
  if (!btn) return;
  const id = btn.dataset.id;
  esegui(id);
});

Vantaggi:

  • Meno listener = meno memoria.
  • Funziona anche per elementi aggiunti dopo (dinamici).
  • Cleanup più semplice (un solo removeEventListener).

Svantaggi:

  • stopPropagation di un figlio rompe la delegazione del genitore.
  • Bug di “ho cliccato qui ma l’handler vede e.target sbagliato”: usa closest.

React: stale closure (di nuovo)

Già visto nel modulo 7.3, ma in contesto eventi:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    function onKey(e) {
      if (e.key === "Enter") console.log(count);  // sempre 0
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);  // dipendenze vuote → closure stale

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Fix opzioni:

  1. Aggiungi count a dipendenze: il listener si re-registra ad ogni cambio.
  2. Usa ref per leggere il valore corrente:
const countRef = useRef(count);
countRef.current = count;

Vue: reactivity caveats

In Vue, alcuni cambi non sono reactive:

const state = reactive({ items: [] });
state.items[0] = "ciao";  // non triggera reactivity in Vue 2
state.items.splice(0, 1, "ciao");  // ok

Vue 3 con Proxy risolve la maggior parte di questi casi, ma alcune operazioni (Map, Set) richiedono ancora reactive esplicito.

Svelte: store leak

<script>
  import { store } from "./store.js";
  $: console.log($store);  // sottoscrizione automatica
</script>

Sottoscrizione viene rimossa al destroy del componente. Se invece sottoscrivi manualmente, devi ricordare di unsubscribe:

const unsub = store.subscribe(v => console.log(v));
onDestroy(unsub);

Esercizio guidato

Crea una piccola pagina con:

  1. Un pulsante che incrementa un contatore. Aggiungi due addEventListener per errore (es. dentro un if che gira due volte). Diagnostica con tab Event Listeners.
  2. Un input dentro una div: registra listener su entrambi. Sperimenta stopPropagation e preventDefault.
  3. Una lista di item con delegazione: usa un solo listener sul container.
  4. In framework di tua scelta: riproduci e correggi una stale closure su un listener keydown.

Errori frequenti del principiante

  • Listener inline che non si possono rimuovere.
  • Aggiungere listener in useEffect senza cleanup.
  • stopPropagation “preventivo” che rompe altre cose.
  • Sottoscrizioni manuali a store senza unsubscribe.