
React 19: "Too many re-renders", l'errore del loop infinito spiegato
Perché React lancia 'Too many re-renders. React limits the number of renders...' e le due cause più comuni: setState nel corpo del render e dipendenze instabili negli effect.
Cambi un paio di righe, salvi, e il browser si blocca per un secondo prima di buttarti in console questo:
⛔ Errore
Too many re-renders. React limits the number of renders to prevent an infinite loop.
Il messaggio ti dice che React ha intercettato il loop prima che il tab andasse davvero in crash — è la buona notizia. La cattiva è che non ti dice quale riga l’ha causato. Quasi sempre il colpevole è uno di due pattern, entrambi facilissimi da scrivere per errore.
Causa #1: chiamare setState direttamente nel corpo del render
È il caso classico, e a prima vista sembra ragionevole:
function Counter() {
const [count, setCount] = useState(0);
// Gira ad ogni render — incluso quello che ha appena innescato
setCount(count + 1);
return <p>{count}</p>;
}
Ogni chiamata a setCount pianifica un re-render. Quel re-render riesegue il corpo del componente, che richiama setCount, che pianifica un altro re-render — all’infinito. La funzione di render di React deve essere pura: può leggere lo stato, ma non può scriverlo incondizionatamente come parte del render.
La correzione è quasi sempre spostare l’aggiornamento dello stato in un event handler o in un effect che gira solo quando cambia qualcosa di specifico:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
⚠ Una variante più subdola
Succede anche indirettamente, quando chiami setState condizionalmente durante il render per
“sincronizzare” le props nello stato: if (value !== prevValue) setState(value). Sembra innocuo
grazie all’if, ma se la condizione risulta vera ad ogni render — per esempio perché value è un
nuovo oggetto letterale ogni volta — sei di nuovo in un loop infinito. Vedi la causa #2 qui sotto
per capire esattamente perché succede.
Causa #2: un oggetto o array ricreato ad ogni render come dipendenza
Questa è più subdola perché la chiamata a setState è tranquillamente dentro un useEffect, il che sembra corretto:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// Nuovo oggetto ad ogni render!
const options = { limit: 10, query };
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]); // options non è mai === all'options precedente
return (
<ul>
{results.map((r) => (
<li key={r.id}>{r.text}</li>
))}
</ul>
);
}
options è un oggetto letterale nuovo di zecca ad ogni render, quindi non è mai uguale al precedente per riferimento. React riesegue l’effect ogni volta, l’effect chiama setResults, che innesca un re-render, che crea un nuovo oggetto options — un loop infinito, solo un passaggio più in là rispetto alla causa #1.
Due modi per risolverlo:
// Opzione A: dipendi dai valori primitivi, non dall'oggetto
useEffect(() => {
fetchResults({ limit: 10, query }).then(setResults);
}, [query]);
// Opzione B: memoizza l'oggetto stesso
const options = useMemo(() => ({ limit: 10, query }), [query]);
useEffect(() => {
fetchResults(options).then(setResults);
}, [options]);
L’opzione A è di solito più semplice ed è quella che sceglierei per prima — dipendi dai valori grezzi che cambiano davvero, e costruisci l’oggetto dentro l’effect dove la sua identità non conta.
💡 Consiglio
Con il React Compiler, buona parte di questa classe di bug diventa più facile da evitare perché il
Compiler memoizza automaticamente oggetti e funzioni al posto tuo — ma non riscrive una chiamata
setState che sta direttamente nel corpo del render, quindi la causa #1 resta interamente a tuo
carico. Approfondisco dove la memoizzazione del Compiler aiuta davvero nell’articolo dedicato al
React Compiler.
Come trovare davvero la riga incriminata
Il messaggio d’errore da solo non punta al tuo codice, ma due strumenti sì:
- L’overlay di React in sviluppo di solito include uno stack di componenti subito sotto l’errore — leggilo dall’alto verso il basso, il primo componente scritto da te (non un wrapper di libreria) è il punto di partenza migliore.
- Commenta gli aggiornamenti di stato uno alla volta. Sembra rozzo, ma per un loop con più coppie
useEffect/useStatenello stesso componente, disabilitare temporaneamente ogni chiamatasetXfinché il loop non si ferma è spesso più veloce che leggere stack trace generati.
FAQ
❓ 'Maximum update depth exceeded' è lo stesso errore?
Sì — è il messaggio Error sottostante che React lancia; “Too many re-renders” è il testo più
amichevole mostrato dall’overlay di errore di React in sviluppo. Entrambi indicano lo stesso
problema.
❓ useMemo o React.memo possono causarlo?
Non da soli — influenzano solo se un componente salta un render, non se ne pianifica uno. Il loop
nasce sempre da una chiamata setState che scatta (di fatto) ad ogni render;
useMemo/React.memo possono nascondere o rivelare il pattern, ma non sono la causa di fondo.
❓ Succede solo con gli hook?
È più comune con useState/useEffect, ma lo stesso tipo di bug esiste anche nei componenti a
classi — chiamare this.setState dentro render(), o dentro componentDidUpdate senza una
condizione adeguata, produce lo stesso identico loop infinito.
Conclusione
“Too many re-renders” riconduce sempre a un aggiornamento di stato che scatta incondizionatamente, che sia una chiamata setState piazzata direttamente nel corpo del render, o una nascosta dentro un effect la cui dipendenza viene ricreata ad ogni render. Una volta che sai cercare esattamente questi due pattern, la correzione è di solito una modifica di una riga — la parte difficile è capire quale delle tue coppie useState/useEffect è quella incriminata.
Riferimenti

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.