L03 - Stato e Hooks in React | DevNotes Interattiva - Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
📘 GUIDE · React + Vite · Lezione 3 di 10
Docente: Prof. Giagnotti · Sviluppo Web Moderno

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.

⏱ ~25 min lettura 🎯 Livello: Principiante 🧠 Focus: useState, useEffect, eventi ⚗️ Lab Pratico — lab-03-state-hooks.md ↗ 🧩 Serie: DevNotes
✓Comprendere perché una variabile locale non basta e come useState innesca il re-render
✓Gestire eventi SyntheticEvent e costruire form con input controllati e validazione
✓Applicare il pattern lift state up per far comunicare NoteForm e NoteList tramite App.jsx
✓Usare useEffect nelle tre forme (mount, update, unmount) con cleanup per timer e fetch
✓Rispettare le regole degli Hooks e l'immutabilità con spread, filter e map

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.

JSX · Approccio intuitivo che non funziona
// ❌ Approccio intuitivo — non funziona function Counter() { let count = 0 return ( <div> <p>Hai cliccato {count} volte</p> <button onClick={() => count++}>Incrementa</button> </div> ) }

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.

📌 Definizione da ricordare — Stato

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.

💬 Tip del docente — debug del primo stato

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.

JSX · useState base
import { useState } from 'react' function Counter() { // count → il valore corrente (inizia a 0) // setCount → la funzione per aggiornarlo const [count, setCount] = useState(0) return ( <div> <p>Hai cliccato {count} volte</p> <button onClick={() => setCount(count + 1)}>Incrementa</button> </div> ) }

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.

JSX · Forma funzionale sicura
// ❌ Pericoloso: usa il valore della closure (potenzialmente stale) setCount(count + 1) // ✅ Sicuro: prev è garantito essere l'ultimo valore aggiornato setCount(prev => prev + 1) // Esempio concreto: tre setCount consecutivi nello stesso handler <button onClick={() => { setCount(prev => prev + 1) setCount(prev => prev + 1) setCount(prev => prev + 1) // risultato: +3 ✅ (senza forma funzionale: +1) }}>+3</button>
⚠️ Errore comune — stale closure nel batching

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.

JSX · Oggetti: mai mutare direttamente
const [nota, setNota] = useState({ titolo: '', testo: '' }) // ❌ Sbagliato: muta l'oggetto → stesso riferimento → nessun re-render nota.titolo = 'Nuova' setNota(nota) // ✅ Corretto: spread crea un nuovo oggetto → riferimento diverso → re-render setNota({ ...nota, titolo: 'Nuova' })
JSX · Array: filter, map, spread
const [lista, setLista] = useState([]) // ❌ Sbagliato: push muta l'array originale lista.push({ id: 1, testo: 'Ciao' }) setLista(lista) // ✅ Aggiungere — spread produce un nuovo array setLista(prev => [...prev, { id: 1, testo: 'Ciao' }]) // ✅ Rimuovere per id — filter restituisce sempre un nuovo array setLista(prev => prev.filter(n => n.id !== idDaRimuovere)) // ✅ Modificare un elemento — map restituisce sempre un nuovo array setLista(prev => prev.map(n => n.id === id ? { ...n, testo: 'Modificato' } : n))
📌 Regola d'oro — Immutabilità

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.

💬 Tip del docente — quando usare più useState

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.

HTMLReact JSXQuando scatta
onclickonClickClick del mouse / tap mobile
onchangeonChangeOgni modifica a un input
onsubmitonSubmitInvio di un form
onkeydownonKeyDownTasto premuto
onfocus / onbluronFocus / onBlurFocus / perdita focus
onmouseenteronMouseEnterHover

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.

JSX · NoteForm.jsx con input controllati e validazione
// NoteForm.jsx — form con input controllati import { useState } from 'react' function NoteForm({ onAggiungi }) { const [titolo, setTitolo] = useState('') const [testo, setTesto] = useState('') const [errore, setErrore] = useState('') function handleSubmit(e) { e.preventDefault() // blocca il reload della pagina if (!titolo.trim()) { setErrore('Il titolo è obbligatorio.') return } onAggiungi({ titolo: titolo.trim(), testo: testo.trim() }) setTitolo('') // reset dopo l'invio setTesto('') setErrore('') } return ( <form onSubmit={handleSubmit} noValidate> <input value={titolo} // ← controllato dallo stato onChange={e => setTitolo(e.target.value)} // ← ogni tasto aggiorna lo stato placeholder="Titolo della nota" /> <textarea value={testo} onChange={e => setTesto(e.target.value)} placeholder="Scrivi qui il contenuto..." /> <button type="submit" disabled={!titolo.trim()}>+ Aggiungi nota</button> {errore && <p className="errore">{errore}</p>} </form> ) } export default NoteForm
🎯 Perché controlled component — vantaggi pratici

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.

App.jsx — note[] + aggiungiNota + eliminaNota NoteForm — onAggiungi ↑ NoteList — note[] ↓ + onElimina

Lift state up: lo stato vive nel genitore comune e viene passato ai figli come props

JSX · App.jsx con stato sollevato
// App.jsx — lo stato note[ ] vive qui import { useState } from 'react' import NoteForm from './components/NoteForm/NoteForm' import NoteList from './components/NoteList/NoteList' function App() { const [note, setNote] = useState([ { id: 1, titolo: 'Prima nota', testo: 'Contenuto...', tag: 'idea', data: new Date() }, ]) function aggiungiNota({ titolo, testo }) { const nuova = { id: Date.now(), titolo, testo, tag: 'idea', data: new Date() } setNote(prev => [nuova, ...prev]) // nuova in cima } function eliminaNota(id) { setNote(prev => prev.filter(n => n.id !== id)) } return ( <main> <NoteForm onAggiungi={aggiungiNota} /> <NoteList note={note} onElimina={eliminaNota} /> </main> ) } export default App
💬 Tip del docente — quando sollevare

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.

📌 Definizione — useEffect

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

JSX · Tre forme di useEffect
import { useEffect } from 'react' // 1. Senza array → esegue dopo OGNI rendering (raramente utile) useEffect(() => { console.log('Ogni re-render') }) // 2. Array vuoto [] → esegue UNA SOLA VOLTA al mount useEffect(() => { console.log('Componente montato') }, []) // 3. [deps] → mount + ogni volta che deps cambia useEffect(() => { document.title = `DevNotes — ${note.length} note` }, [note.length]) // ← si aggiorna ogni volta che il numero di note cambia
MOUNT

useEffect(fn, [])

Esegue una sola volta dopo il primo rendering. Ideale per fetch iniziali e setup.

UPDATE

useEffect(fn, [dep])

Mount + ogni volta che dep cambia. Sincronizza titolo, localStorage, animazioni.

UNMOUNT

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.

JSX · Orologio con timer + cleanup corretto
// Orologio.jsx — timer con cleanup corretto import { useState, useEffect } from 'react' function Orologio() { const [ora, setOra] = useState(new Date()) useEffect(() => { // avvia il timer al mount const id = setInterval(() => setOra(new Date()), 1000) // cleanup: React chiama questa funzione prima di smontare il componente // senza questo return, il timer continua a girare anche a componente rimosso return () => clearInterval(id) }, []) return <p>{ora.toLocaleTimeString()}</p> }
⚠️ Errore classico — memory leak senza cleanup

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.

JSX · Fetch sicura con AbortController
useEffect(() => { // AbortController permette di cancellare la richiesta in volo const controller = new AbortController() async function caricaNote() { try { const res = await fetch('/api/note', { signal: controller.signal }) const data = await res.json() setNote(data) } catch (err) { if (err.name !== 'AbortError') setErrore(err.message) } } caricaNote() // cleanup: se il componente viene smontato prima della risposta, cancella la fetch return () => controller.abort() }, [])

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.

#RegolaPerché
1Chiama gli Hooks solo al top levelNon 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.
2Chiama gli Hooks solo da componenti React o Hook personalizzatiNon da funzioni JavaScript normali. Questo mantiene la logica con stato tracciabile, testabile e visibile in React DevTools.
JSX · Regole degli Hooks — errato vs corretto
// ❌ Violazione regola 1 — Hook dentro un if function MyComponent({ attivo }) { if (attivo) { const [dato, setDato] = useState(0) // ← React non riesce a tracciare l'ordine } } // ✅ Corretto — Hook sempre chiamato, condizione dentro il corpo function MyComponent({ attivo }) { const [dato, setDato] = useState(0) if (!attivo) return null // ... resto del componente }

7. Verifica / Esercizio pratico

🔬 Attività — Lab 03

Apri il lab pratico dal meta-pill giallo in alto. Esegui tutti i passaggi e rispondi:

  1. Crea NoteForm.jsx con due input controllati (titolo + testo) e validazione: il pulsante è disabilitato se il titolo è vuoto e preventDefault() blocca il reload.
  2. Sposta lo stato note[] in App.jsx. Implementa aggiungiNota con spread [nuova, ...prev] e eliminaNota con filter. Passa tutto ai figli tramite props.
  3. Aggiungi useEffect in App che aggiorna document.title con DevNotes — {n} note ogni volta che note.length cambia. Aggiungi un componente Orologio con setInterval + cleanup e verifica in React DevTools che non resti attivo dopo lo smontaggio.
✅ Soluzione ragionata

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.

✓useState restituisce [valore, setter]. Chiamare il setter notifica React e pianifica un re-render. Le variabili locali non triggherano nessun aggiornamento.
✓Non mutare mai lo stato. Usa spread per oggetti, filter/map/spread per array: nuovo riferimento = segnale per React.
✓Forma funzionale setState(prev => ...) quando il nuovo valore dipende dal precedente — evita stale closure.
✓Componenti controllati: value + onChange collegati allo stato. Lo stato è unica fonte di verità per validazione e reset.
✓Lift state up: quando fratelli condividono dati, stato sale al genitore comune e scende via props.
✓useEffect(fn, []) mount, [dep] mount+update su dep, return cleanup per timer/listener/fetch con AbortController.
✓Regole Hooks: top level, solo in componenti React o custom hook. ESLint ti avvisa in tempo reale.

📚 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.