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.
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:
addEventListenerchiamato due volte senza rimozione.- Componente che si rimonta senza cleanup.
- Listener attaccato a
documentinvece che al nodo specifico.
Bubbling e capture
Quando l’utente clicca su <button> dentro <div> dentro <section>, l’evento attraversa 3 fasi:
- Capture: dall’
htmlgiù verso il target. - Target: sul nodo stesso.
- 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:
stopPropagationdi un figlio rompe la delegazione del genitore.- Bug di “ho cliccato qui ma l’handler vede
e.targetsbagliato”: usaclosest.
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:
- Aggiungi
counta dipendenze: il listener si re-registra ad ogni cambio. - Usa
refper 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:
- Un pulsante che incrementa un contatore. Aggiungi due
addEventListenerper errore (es. dentro unifche gira due volte). Diagnostica con tab Event Listeners. - Un input dentro una div: registra listener su entrambi. Sperimenta
stopPropagationepreventDefault. - Una lista di item con delegazione: usa un solo listener sul container.
- 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
useEffectsenza cleanup. stopPropagation“preventivo” che rompe altre cose.- Sottoscrizioni manuali a store senza unsubscribe.