# useEffect viene eseguito due volte: è StrictMode, non un bug

In sviluppo React monta, smonta e rimonta ogni componente per esporre effetti senza pulizia. La correzione è scrivere la cleanup, non disattivare StrictMode.

> Pagina: https://ui.eneawork.it/blog/useeffect-due-volte-in-sviluppo
> Markdown: https://ui.eneawork.it/blog/useeffect-due-volte-in-sviluppo.md

*2026-11-01*

Risposta breve: non è un bug. Con <StrictMode> React 18 e 19 eseguono ogni effetto due volte in sviluppo, mount, cleanup, mount, per farti vedere gli effetti che non sanno annullarsi.

La domanda «Why useEffect running twice and how to handle it well in React?» vale 174 voti su Stack Overflow (letto il 31 ottobre 2026 con la Stack Exchange API): fonte. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026, e verificati con una chiamata diretta all'endpoint. Ogni comando qui sotto è eseguibile in locale: nessun passaggio richiede un account.

## Il meccanismo: un test, non una doppia esecuzione

StrictMode chiede a React di provare il ciclo completo: monta, esegue l'effetto, esegue la cleanup, rimonta ed esegue di nuovo. L'obiettivo è dimostrare che l'effetto sa tornare indietro. Un effetto che sopravvive al ciclo è un effetto che si comporterà bene quando il componente verrà davvero rimontato.

Il comportamento esiste solo in sviluppo e solo con il wrapper attivo. In produzione il ciclo è uno. Attenzione a un dettaglio: anche il Fast Refresh rimonta i componenti mentre lavori, quindi due richieste possono arrivare da un salvataggio e non da StrictMode.

mount → effect → cleanup → effect # in produzione mount → effect

Il doppio passaggio non è una penalità di prestazioni: è un test. Un effetto che non supporta il ciclo è un effetto che si romperà su un rimontaggio reale, cioè su un cambio di rotta o su una chiave diversa.

## La correzione: cleanup simmetrica

useEffect(() => { const controller = new AbortController(); fetch("/api/orders", { signal: controller.signal }) .then((r) => r.json()) .then(setOrders) .catch((e) => { if (e.name !== "AbortError") throw e; }); return () => controller.abort(); }, []);

Senza abort la prima risposta può arrivare dopo la seconda e sovrascrivere lo stato con dati vecchi. È il classico difetto che in sviluppo sembra un doppio fetch e in produzione diventa un dato sbagliato, difficile da riprodurre perché dipende dall'ordine di arrivo delle risposte.

## Le sottoscrizioni

useEffect(() => { const mq = window.matchMedia("(min-width: 768px)"); const onChange = () => setWide(mq.matches); onChange(); mq.addEventListener("change", onChange); return () => mq.removeEventListener("change", onChange); }, []);

La regola è generale: ogni addEventListener, setInterval, subscribe o observe deve avere la sua rimozione. Se la cleanup è vuota, il doppio avvio sta facendo il suo lavoro e ti sta mostrando un leak.

## Diagnosi: conta le coppie, non le esecuzioni

Un contatore con useRef e due log dicono subito se il ciclo è sano. Il numero assoluto conta poco: conta la sequenza.

const runs = useRef(0); useEffect(() => { runs.current += 1; console.log(`effect #${runs.current}`, userId); return () => console.log(`cleanup #${runs.current}`); }, [userId]);

Due effetti con una cleanup in mezzo sono il segnale atteso. Dieci effetti in fila indicano una dipendenza che cambia identità a ogni render. Un solo effetto senza cleanup significa che il componente non si è mai smontato, e il problema è altrove.

## Quando la correzione comune peggiora le cose

La prima scorciatoia è passare a dipendenze vuote per fermare il ciclo. L'effetto smette di ripartire e continua a leggere i valori del primo render: chiedi i dati di un utente e ricevi quelli del precedente. L'errore si sposta da «due richieste» a «dati di un altro utente», che in produzione è molto peggio.

La seconda è una guardia con un flag a livello di modulo, del tipo «se ho già girato, esci». Funziona finché il componente non viene rimontato: la variabile sopravvive al rimontaggio, l'effetto non riparte e la sottoscrizione resta chiusa per sempre. Una guardia con useRef è meno dannosa, ma nella stessa StrictMode salta la seconda esecuzione dopo che la cleanup ha già chiuso tutto.

## Un limite da mettere in conto: il test ha effetti collaterali

Il ciclo di prova esegue davvero il codice, due volte. Se l'effetto invia un POST, in sviluppo il server riceve due scritture. StrictMode non può sapere se l'operazione è idempotente: la doppia esecuzione è un test distruttivo per definizione. La conseguenza pratica è che l'effetto deve essere sicuro da ripetere, oppure l'invio va spostato su un gestore di evento con una chiave di idempotenza.

## Come prevenirlo

## Dove continuare

Nel catalogo: componenti React, dashboard con dati e icone. Le guide: checklist accessibilità e peso di una libreria UI; il confronto: copy-paste o headless.

## FAQ

### Il doppio avvio invia due richieste al server?

Sì, in sviluppo. Per questo la cleanup deve annullare la prima con `AbortController`: senza, la prima risposta può sovrascrivere la seconda con dati vecchi.

### Posso togliere StrictMode?

Puoi, ma perdi il controllo che React offre proprio per trovare effetti fragili. Togli il wrapper e il leak resta, con la differenza che non hai più un ambiente che lo mostra.

### Succede anche in produzione?

Non per StrictMode. Succede quando il componente viene rimontato davvero: cambio rotta, chiave diversa, sospensione, oppure un Fast Refresh durante lo sviluppo.

### Vale anche per `useLayoutEffect`?

Sì, la regola è identica: ogni sottoscrizione o richiesta deve avere una cleanup simmetrica. Cambia il momento in cui il ciclo viene eseguito, non il contratto.

### Come verifico che la cleanup funzioni?

Aggiungi un log nella cleanup e monta il componente due volte: deve comparire prima del secondo effetto, senza avvisi di stato aggiornato dopo lo smontaggio.
