Lezione 3 — Stato e Hooks: DevNotes Interattiva
Nella lezione precedente hai organizzato DevNotes in componenti statici. Ora lo rendiamo vivo: useState per rendere l'UI reattiva, gestione eventi, input controllati, lift state up e useEffect per sincronizzare il componente con il mondo esterno. Dopo questa lezione DevNotes aggiunge ed elimina note davvero.
1. Cos'è lo stato — perché non basta una variabile
Nella lezione precedente hai organizzato DevNotes in componenti — Header, NoteCard, NoteList — e hai capito come le props portano i dati dall'alto verso il basso nell'albero. I componenti però erano statici: mostravano dati fissi e non reagivano a nulla. Un click sul pulsante "elimina" non faceva niente. Il campo di ricerca era decorativo.
Questo è il limite dei componenti senza stato. In questa lezione lo risolvi: introduci useState, gestisci gli eventi utente, sollevi lo stato al genitore comune e usi useEffect per sincronizzare il componente con il mondo esterno. Dopo questa lezione DevNotes sarà una vera applicazione interattiva.
Clicchi il pulsante: count sale a 1, poi a 2 — ma sullo schermo rimane sempre zero. Perché? Perché React non ha ricevuto nessuna notifica. Quando riesegue la funzione Counter() per aggiornare il DOM virtuale, count riparte da zero. React non osserva le variabili locali. Produce l'interfaccia, si ferma, e aspetta che qualcuno lo avvisi esplicitamente che qualcosa è cambiato.
Lo stato in React è un valore che persiste tra un rendering e l'altro e che, quando cambia, pianifica un nuovo rendering del componente. La funzione useState restituisce il valore corrente e la funzione setter: chiamare il setter è l'unico canale ufficiale per notificare React che qualcosa è cambiato.
Quando spiego lo stato in classe apro sempre React DevTools > Components e cerco la voce State. Se vedi che setCount cambia ma il componente non si aggiorna, stai quasi certamente mutando direttamente l'oggetto. Controlla sempre che tu stia creando un nuovo riferimento con spread.
2. useState: anatomia, rendering e aggiornamento funzionale
La sintassi è semplice: chiami useState con il valore iniziale e destrutturi il risultato in un array di due elementi.
Quando clicchi il pulsante, setCount notifica React che il valore è cambiato. React pianifica un nuovo rendering di Counter: la funzione viene rieseguita, e questa volta useState(0) non restituisce 0 — restituisce il valore aggiornato salvato internamente. Solo il nodo del DOM che mostra il contatore viene toccato; il pulsante rimane intatto. Questo è il diffing del virtual DOM al lavoro.
Aggiornamento funzionale — quando usarlo
Se il nuovo valore dipende da quello precedente, usa sempre la forma funzionale del setter. React può raggruppare più aggiornamenti in batch, quindi il valore di count nella closure potrebbe essere già obsoleto nel momento in cui viene letto.
Se chiami tre volte setCount(count + 1) nello stesso handler, React raggruppa (batch) gli update e vede sempre lo stesso count, quindi incrementa solo di 1. Con setCount(prev => prev + 1) ogni chiamata riceve il valore più recente. Regola: quando il nuovo stato dipende dal vecchio, usa sempre la callback prev => ...
3. Stato complesso: oggetti, array e la regola dell'immutabilità
React usa il confronto per riferimento per capire se lo stato è cambiato. Modificare direttamente un oggetto o un array lascia invariato il riferimento: React non vede nessuna differenza e non ri-renderizza. È il bug invisibile più comune per chi inizia.
Non mutare mai lo stato. Per gli oggetti usa lo spread { ...obj, chiave: nuovoValore }. Per gli array usa filter(), map() e lo spread [...arr]. Queste operazioni producono sempre un nuovo riferimento — che è esattamente ciò che React controlla per decidere se ri-renderizzare.
All'inizio è tentante mettere tutto in un unico oggetto gigante { titolo, testo, errore, loading }. In classe insegno il contrario: usa uno useState per ogni concetto indipendente. Titolo e testo possono stare insieme, ma errore e lista note meritano stati separati. È più facile aggiornare e debuggare.
4. Gestione eventi e componenti controllati — NoteForm
React normalizza gli eventi del browser in un sistema di SyntheticEvent: un wrapper cross-browser che espone la stessa API su Chrome, Firefox e Safari. Nella pratica usi gli stessi nomi dell'HTML — ma in camelCase e come props JSX, non come attributi stringa.
| HTML | React JSX | Quando scatta |
|---|---|---|
onclick | onClick | Click del mouse / tap mobile |
onchange | onChange | Ogni modifica a un input |
onsubmit | onSubmit | Invio di un form |
onkeydown | onKeyDown | Tasto premuto |
onfocus / onblur | onFocus / onBlur | Focus / perdita focus |
onmouseenter | onMouseEnter | Hover |
Componenti controllati
Un componente controllato è un <input> il cui valore è sempre determinato dallo stato React — non dal DOM. È il pattern fondamentale per gestire form in React: lo stato è l'unica fonte di verità, e ogni battitura aggiorna lo stato che a sua volta aggiorna il campo.
Con un input controllato lo stato React è la sola fonte di verità. Puoi validare al volo, trasformare l'input (es. uppercase), disabilitare il pulsante se il campo è vuoto, o azzerare il form dopo l'invio — tutto senza toccare il DOM direttamente. È più codice di un form HTML classico, ma totalmente prevedibile.
5. Lift state up — far comunicare fratelli tramite App.jsx
In DevNotes il form NoteForm aggiunge note, e NoteList le mostra. I due componenti sono fratelli: nessuno dei due può leggere lo stato dell'altro. La soluzione è sollevare lo stato al genitore comune — App.jsx — e passarlo ai figli tramite props.
Lift state up: lo stato vive nel genitore comune e viene passato ai figli come props
Regola pratica che uso in laboratorio: se due componenti fratelli devono condividere un dato, quel dato non appartiene a nessuno dei due — appartiene al genitore. Sollevalo, poi passa setter giù come prop. Se diventa troppo profondo, è il momento di Context o Zustand, ma non in questa lezione.
6. useEffect, cleanup e ciclo di vita — timer e fetch sicuri
Tutto ciò che hai scritto finora è puro: dati in ingresso → JSX in uscita. Ma le applicazioni reali devono interagire con cose esterne al flusso di React: il titolo del documento, le API, i timer, i WebSocket. Queste interazioni si chiamano side effects — effetti collaterali — perché escono dal flusso di rendering puro.
useEffect(fn, deps) esegue la funzione fn dopo il rendering, ogni volta che uno dei valori in deps cambia. Se deps è un array vuoto [], l'effetto gira una sola volta al mount. Se deps viene omesso, gira ad ogni rendering. Il valore di ritorno di fn, se presente, è la funzione di cleanup.
Le tre forme di useEffect
useEffect(fn, [])
Esegue una sola volta dopo il primo rendering. Ideale per fetch iniziali e setup.
useEffect(fn, [dep])
Mount + ogni volta che dep cambia. Sincronizza titolo, localStorage, animazioni.
return () => fn()
Cleanup eseguito prima di smontare. Sempre necessario per timer e listener.
Il cleanup — evitare i memory leak
Alcuni effetti aprono risorse — timer, listener, connessioni — che devono essere chiuse quando il componente viene rimosso o quando l'effetto viene rieseguito. useEffect supporta una funzione di cleanup: basta restituirla dall'interno dell'effetto.
Se dimentichi il cleanup di un setInterval o di un addEventListener, la risorsa continua a vivere in memoria anche dopo che il componente è sparito dal DOM. In un'app con tante route questo causa memory leak progressivi e aggiornamenti di stato su componenti già smontati — che React segnala con un warning in console. Regola: ogni setInterval vuole il suo clearInterval nel return.
useEffect e le chiamate fetch con AbortController
Un pattern che incontrerai spesso è usare useEffect per caricare dati al mount del componente. In L06 costruiremo un custom hook dedicato; per ora è importante capire la struttura base e il perché del cleanup con AbortController.
Le regole degli Hooks
Gli Hooks hanno due regole inviolabili. Il plugin ESLint eslint-plugin-react-hooks — già incluso nel template Vite — ti segnala in real-time se le violi. Non ignorare mai quei warning: quasi sempre indicano un bug reale.
| # | Regola | Perché |
|---|---|---|
| 1 | Chiama gli Hooks solo al top level | Non dentro if, loop o funzioni annidate. React si affida all'ordine di chiamata per associare lo stato corretto a ogni Hook: se l'ordine cambia tra i rendering, tutto si rompe. |
| 2 | Chiama gli Hooks solo da componenti React o Hook personalizzati | Non da funzioni JavaScript normali. Questo mantiene la logica con stato tracciabile, testabile e visibile in React DevTools. |
7. Verifica / Esercizio pratico
🔬 Attività — Lab 03
Apri il lab pratico dal meta-pill giallo in alto. Esegui tutti i passaggi e rispondi:
- Crea
NoteForm.jsxcon due input controllati (titolo + testo) e validazione: il pulsante è disabilitato se il titolo è vuoto epreventDefault()blocca il reload. - Sposta lo stato
note[]inApp.jsx. ImplementaaggiungiNotacon spread[nuova, ...prev]eeliminaNotaconfilter. Passa tutto ai figli tramite props. - Aggiungi
useEffectin App che aggiornadocument.titleconDevNotes — {n} noteogni volta chenote.lengthcambia. Aggiungi un componente Orologio consetInterval+ cleanup e verifica in React DevTools che non resti attivo dopo lo smontaggio.
1. Ogni input ha value={state} + onChange che chiama setState. trim() per validare, disabled={!titolo.trim()} per UX. 2. Lo stato vive in App, NoteForm chiama onAggiungi (prop) con nuovo oggetto, NoteList chiama onElimina. Sempre prev => [...] per sicurezza sul batching. 3. useEffect con [note.length] come dipendenza aggiorna titolo. Orologio: useEffect con [] al mount avvia interval, return () => clearInterval(id) al unmount. In StrictMode React monta/smonta due volte in dev per testare cleanup — è normale vedere due log.
8. Riepilogo e concetti chiave
📚 Prossimo passo — L04 Stile: CSS Modules e Tailwind
Ora che DevNotes è interattiva, è brutta. Nella prossima lezione le diamo stile professionale con CSS Modules, Tailwind in Vite e utility classes, mantenendo i componenti isolati e riutilizzabili.