00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

useEffect

Cos’è useEffect

useEffect è l’hook di React per eseguire effetti collaterali (side effects) nei componenti funzionali. Un side effect è qualsiasi operazione che interagisce con il mondo esterno al componente: chiamate API, manipolazione del DOM, timer, sottoscrizioni a eventi.

import { useEffect, useState } from "react";

function Titolo({ pagina }) {
  useEffect(() => {
    document.title = `Pagina: ${pagina}`;
  }, [pagina]);

  return <h1>{pagina}</h1>;
}

Sintassi di useEffect

useEffect(
  () => {
    // Codice dell'effetto (eseguito dopo il render)

    return () => {
      // Funzione di cleanup (opzionale)
    };
  },
  [dipendenza1, dipendenza2] // Array di dipendenze (opzionale)
);

L’effetto viene eseguito dopo che React ha aggiornato il DOM, non durante il rendering.

Array di Dipendenze

L’array di dipendenze controlla quando l’effetto viene ri-eseguito.

Nessun Array: Esegui ad Ogni Render

useEffect(() => {
  console.log("Eseguito dopo OGNI render");
});

Questo pattern è raramente utile e può causare problemi di performance. Usalo con estrema cautela.

Array Vuoto: Solo al Mount

useEffect(() => {
  console.log("Eseguito solo al primo render (mount)");

  return () => {
    console.log("Eseguito quando il componente si smonta (unmount)");
  };
}, []);

Con Dipendenze: Quando Cambiano

useEffect(() => {
  console.log(`L'effetto si esegue perché userId è cambiato: ${userId}`);

  // Fetch dati dell'utente
  fetchUtente(userId);
}, [userId]); // Si ri-esegue SOLO quando userId cambia

Come React Confronta le Dipendenze

React usa Object.is() per confrontare i valori delle dipendenze tra un render e l’altro. Per i valori primitivi (numeri, stringhe, booleani) il confronto è per valore. Per oggetti e array è per riferimento.

// Primitivi: confronto per valore
const [count, setCount] = useState(0);
useEffect(() => { ... }, [count]); // Si esegue quando count cambia valore

// Oggetti: confronto per riferimento
const filtri = { categoria: "libri" };  // Nuovo oggetto ad ogni render!
useEffect(() => { ... }, [filtri]); // Si esegue ad OGNI render (bug)

// Soluzione: useMemo o stato
const [filtri, setFiltri] = useState({ categoria: "libri" });
useEffect(() => { ... }, [filtri]); // Si esegue solo quando setFiltri viene chiamato

Funzione di Cleanup

La funzione di cleanup viene eseguita prima che l’effetto venga ri-eseguito e quando il componente si smonta. È essenziale per evitare memory leak.

function Timer() {
  const [secondi, setSecondi] = useState(0);

  useEffect(() => {
    const intervallo = setInterval(() => {
      setSecondi((prev) => prev + 1);
    }, 1000);

    // Cleanup: rimuovi l'intervallo
    return () => clearInterval(intervallo);
  }, []);

  return <p>Tempo: {secondi}s</p>;
}

Ordine di Esecuzione

useEffect(() => {
  console.log("1. Effetto eseguito");

  return () => {
    console.log("2. Cleanup eseguito");
  };
}, [dipendenza]);

// Al mount: "1. Effetto eseguito"
// Quando dipendenza cambia: "2. Cleanup eseguito" → "1. Effetto eseguito"
// Al unmount: "2. Cleanup eseguito"

Fetch di Dati

Uno degli usi più comuni di useEffect è il caricamento di dati da un’API.

function ListaUtenti() {
  const [utenti, setUtenti] = useState([]);
  const [caricamento, setCaricamento] = useState(true);
  const [errore, setErrore] = useState(null);

  useEffect(() => {
    // Flag per evitare aggiornamenti su componente smontato
    let cancellato = false;

    async function caricaUtenti() {
      try {
        setCaricamento(true);
        const risposta = await fetch("https://api.example.com/utenti");

        if (!risposta.ok) {
          throw new Error(`Errore HTTP: ${risposta.status}`);
        }

        const dati = await risposta.json();

        if (!cancellato) {
          setUtenti(dati);
          setErrore(null);
        }
      } catch (err) {
        if (!cancellato) {
          setErrore(err.message);
        }
      } finally {
        if (!cancellato) {
          setCaricamento(false);
        }
      }
    }

    caricaUtenti();

    return () => {
      cancellato = true; // Cleanup: previeni aggiornamenti dopo unmount
    };
  }, []);

  if (caricamento) return <p>Caricamento...</p>;
  if (errore) return <p>Errore: {errore}</p>;

  return (
    <ul>
      {utenti.map((u) => (
        <li key={u.id}>{u.nome}</li>
      ))}
    </ul>
  );
}

Fetch con AbortController

Un approccio più robusto usa AbortController per cancellare richieste in corso.

function DettaglioUtente({ userId }) {
  const [utente, setUtente] = useState(null);
  const [caricamento, setCaricamento] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function caricaUtente() {
      try {
        setCaricamento(true);
        const risposta = await fetch(
          `https://api.example.com/utenti/${userId}`,
          { signal: controller.signal }
        );
        const dati = await risposta.json();
        setUtente(dati);
      } catch (err) {
        if (err.name !== "AbortError") {
          console.error("Errore nel fetch:", err);
        }
      } finally {
        setCaricamento(false);
      }
    }

    caricaUtente();

    return () => controller.abort(); // Cancella la richiesta se userId cambia
  }, [userId]);

  if (caricamento) return <Spinner />;
  if (!utente) return <p>Utente non trovato</p>;

  return <h1>{utente.nome}</h1>;
}

Event Listener

function RilevatoreResize() {
  const [dimensione, setDimensione] = useState({
    larghezza: window.innerWidth,
    altezza: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setDimensione({
        larghezza: window.innerWidth,
        altezza: window.innerHeight,
      });
    };

    window.addEventListener("resize", handleResize);

    // Cleanup: rimuovi l'event listener
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return (
    <p>
      Finestra: {dimensione.larghezza}x{dimensione.altezza}
    </p>
  );
}

Listener su Keyboard

function ScorciatoieTastiera() {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.ctrlKey && e.key === "s") {
        e.preventDefault();
        console.log("Salvataggio...");
      }
      if (e.key === "Escape") {
        console.log("Chiusura modale...");
      }
    };

    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, []);

  return <p>Premi Ctrl+S per salvare, Esc per chiudere</p>;
}

Sincronizzazione con Sistemi Esterni

useEffect è pensato per sincronizzare il componente con sistemi esterni.

function ChatRoom({ roomId }) {
  useEffect(() => {
    // Connessione al sistema esterno (chat)
    const connessione = creaConnessioneChat(roomId);
    connessione.connetti();

    // Cleanup: disconnessione
    return () => connessione.disconnetti();
  }, [roomId]); // Ri-connetti quando cambia stanza

  return <h1>Stanza: {roomId}</h1>;
}

Quando NON Usare useEffect

Molti sviluppatori abusano di useEffect. Ecco i casi in cui non dovresti usarlo.

Trasformare Dati per il Rendering

// SBAGLIATO: useEffect per derivare dati
function ListaFiltrata({ items, filtro }) {
  const [filtrati, setFiltrati] = useState([]);

  useEffect(() => {
    setFiltrati(items.filter((i) => i.categoria === filtro));
  }, [items, filtro]);

  return <Lista items={filtrati} />;
}

// CORRETTO: calcola durante il render
function ListaFiltrata({ items, filtro }) {
  const filtrati = items.filter((i) => i.categoria === filtro);
  return <Lista items={filtrati} />;
}

Gestire Eventi Utente

// SBAGLIATO: useEffect per reagire a un'azione utente
function Carrello() {
  const [prodotto, setProdotto] = useState(null);

  useEffect(() => {
    if (prodotto) {
      aggiungiAlCarrello(prodotto);
    }
  }, [prodotto]);

  return <button onClick={() => setProdotto(item)}>Aggiungi</button>;
}

// CORRETTO: gestisci l'azione nell'handler dell'evento
function Carrello() {
  const handleAggiungi = (item) => {
    aggiungiAlCarrello(item);
  };

  return <button onClick={() => handleAggiungi(item)}>Aggiungi</button>;
}

Reset dello Stato al Cambio di Props

// SBAGLIATO
function ProfiloEditor({ userId }) {
  const [nome, setNome] = useState("");

  useEffect(() => {
    setNome(""); // Reset quando cambia utente
  }, [userId]);
}

// CORRETTO: usa la prop key per forzare il remount
function App({ userId }) {
  return <ProfiloEditor key={userId} userId={userId} />;
}

Effetti Multipli

Separa effetti non correlati in chiamate useEffect distinte.

function Dashboard({ userId }) {
  const [utente, setUtente] = useState(null);
  const [notifiche, setNotifiche] = useState([]);

  // Effetto 1: Carica dati utente
  useEffect(() => {
    fetchUtente(userId).then(setUtente);
  }, [userId]);

  // Effetto 2: Sottoscrivi notifiche (indipendente)
  useEffect(() => {
    const sub = sottoscriviNotifiche(userId, setNotifiche);
    return () => sub.cancella();
  }, [userId]);

  // Effetto 3: Aggiorna titolo pagina (indipendente)
  useEffect(() => {
    document.title = utente ? `Dashboard - ${utente.nome}` : "Dashboard";
  }, [utente]);

  return <div>{/* ... */}</div>;
}

useEffect serve per sincronizzare il componente con sistemi esterni: API, DOM, timer, WebSocket. Ricorda sempre di gestire il cleanup, usa l’array di dipendenze correttamente, e non usare useEffect per logica che può essere gestita durante il render o negli event handler. Per il data fetching in applicazioni reali, considera librerie come TanStack Query che gestiscono caching, retry e deduplicazione automaticamente.