Sincronizzare con gli Effetti

Nota bene

Questa pagina è stata tradotta automaticamente e supervisionata da un maintainer. Un’ulteriore revisione da parte della community sarebbe comunque utile. Migliora questa traduzione.

Alcuni componenti devono sincronizzarsi con sistemi esterni. Per esempio, potresti voler controllare un componente non-React in base allo state di React, impostare una connessione al server o inviare un log di analytics quando un componente appare sullo schermo. Gli Effetti ti permettono di eseguire del codice dopo la renderizzazione, così da sincronizzare il tuo componente con un sistema esterno a React.

Imparerai

  • Cosa sono gli Effetti
  • In che modo gli Effetti sono diversi dagli eventi
  • Come dichiarare un Effetto nel tuo componente
  • Come evitare di rieseguire un Effetto inutilmente
  • Perché gli Effetti girano due volte in modalità di sviluppo e come risolvere il problema

Cosa sono gli Effetti e in che modo sono diversi dagli eventi?

Prima di arrivare agli Effetti, devi conoscere due tipi di logica all’interno dei componenti React:

  • Il codice di renderizzazione (introdotto in Descrivere l’UI) vive al top level del tuo componente. Qui prendi le props e lo state, li trasformi e restituisci il JSX che vuoi vedere sullo schermo. Il codice di renderizzazione deve essere puro. Come una formula matematica, dovrebbe solo calcolare il risultato, senza fare altro.

  • I gestori di eventi (introdotti in Aggiungere le Interazioni) sono funzioni annidate all’interno dei tuoi componenti che fanno cose invece di limitarsi a calcolarle. Un gestore di eventi potrebbe aggiornare un campo di input, inviare una richiesta HTTP POST per acquistare un prodotto o navigare l’utente verso un’altra schermata. I gestori di eventi contengono “effetti collaterali” (cambiano lo state del programma) causati da un’azione specifica dell’utente (per esempio, un click su un pulsante o la digitazione).

A volte questo non basta. Considera un componente ChatRoom che deve connettersi al server di chat ogni volta che è visibile sullo schermo. Connettersi a un server non è un calcolo puro (è un effetto collaterale), quindi non può avvenire durante la renderizzazione. Tuttavia, non c’è un singolo evento particolare come un click che fa apparire ChatRoom.

Gli Effetti ti permettono di specificare effetti collaterali causati dalla renderizzazione stessa, piuttosto che da un evento particolare. Inviare un messaggio in chat è un evento perché è causato direttamente dall’utente che clicca un pulsante specifico. Tuttavia, impostare una connessione al server è un Effetto perché dovrebbe avvenire indipendentemente da quale interazione ha fatto apparire il componente. Gli Effetti girano alla fine della fase di commit dopo l’aggiornamento dello schermo. È un buon momento per sincronizzare i componenti React con un sistema esterno (come la rete o una libreria di terze parti).

Nota bene

Qui e più avanti in questo testo, “Effetto” con la maiuscola si riferisce alla definizione specifica di React sopra, cioè un effetto collaterale causato dalla renderizzazione. Per riferirci al concetto più ampio della programmazione, diremo “effetto collaterale”.

Potresti non avere bisogno di un Effetto

Non affrettarti ad aggiungere Effetti ai tuoi componenti. Tieni presente che gli Effetti vengono tipicamente usati per “uscire” dal tuo codice React e sincronizzarsi con un sistema esterno. Questo include le API del browser, widget di terze parti, la rete e così via. Se il tuo Effetto regola solo dello state in base ad altro state, potresti non avere bisogno di un Effetto.

Come scrivere un Effetto

Per scrivere un Effetto, segui questi tre passaggi:

  1. Dichiarare un Effetto. Per impostazione predefinita, il tuo Effetto girerà dopo ogni fase di commit.
  2. Specificare le dipendenze dell’Effetto. La maggior parte degli Effetti dovrebbe rieseguirsi solo quando necessario invece che dopo ogni renderizzazione. Per esempio, un’animazione fade-in dovrebbe attivarsi solo quando un componente appare. Connettersi e disconnettersi da una chat room dovrebbe avvenire solo quando il componente appare e scompare, o quando cambia la chat room. Imparerai a controllare questo specificando le dipendenze.
  3. Aggiungere la cleanup se necessario. Alcuni Effetti devono specificare come fermare, annullare o ripulire ciò che stavano facendo. Per esempio, “connect” ha bisogno di “disconnect”, “subscribe” ha bisogno di “unsubscribe” e “fetch” ha bisogno di “cancel” o “ignore”. Imparerai a farlo restituendo una funzione di cleanup.

Vediamo ciascuno di questi passaggi in dettaglio.

Passo 1: Dichiarare un Effetto

Per dichiarare un Effetto nel tuo componente, importa l’Hook useEffect da React:

import { useEffect } from 'react';

Poi, chiamalo al top level del tuo componente e inserisci del codice all’interno del tuo Effetto:

function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}

Ogni volta che il tuo componente viene renderizzato, React aggiornerà lo schermo e poi eseguirà il codice all’interno di useEffect. In altre parole, useEffect “ritarda” l’esecuzione di un pezzo di codice finché quella renderizzazione non si riflette sullo schermo.

Vediamo come puoi usare un Effetto per sincronizzarti con un sistema esterno. Considera un componente React <VideoPlayer>. Sarebbe bello controllare se è in riproduzione o in pausa passandogli una prop isPlaying:

<VideoPlayer isPlaying={isPlaying} />;

Il tuo componente personalizzato VideoPlayer renderizza il tag <video> integrato del browser:

function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
return <video src={src} />;
}

Tuttavia, il tag <video> del browser non ha una prop isPlaying. L’unico modo per controllarlo è chiamare manualmente i metodi play() e pause() sul nodo DOM. Devi sincronizzare il valore della prop isPlaying, che indica se il video dovrebbe essere attualmente in riproduzione, con chiamate come play() e pause().

Prima avremo bisogno di ottenere un ref al nodo DOM <video>.

Potresti essere tentato di chiamare play() o pause() durante la renderizzazione, ma non è corretto:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  if (isPlaying) {
    ref.current.play();  // Calling these while rendering isn't allowed.
  } else {
    ref.current.pause(); // Also, this crashes.
  }

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Il motivo per cui questo codice non è corretto è che cerca di fare qualcosa con il nodo DOM durante la renderizzazione. In React, la renderizzazione dovrebbe essere un calcolo puro del JSX e non dovrebbe contenere effetti collaterali come la modifica del DOM.

Inoltre, quando VideoPlayer viene chiamato per la prima volta, il suo DOM non esiste ancora! Non c’è ancora un nodo DOM su cui chiamare play() o pause(), perché React non sa quale DOM creare finché non restituisci il JSX.

La soluzione qui è avvolgere l’effetto collaterale con useEffect per spostarlo fuori dal calcolo di renderizzazione:

import { useEffect, useRef } from 'react';

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);

useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});

return <video ref={ref} src={src} loop playsInline />;
}

Avvolgendo l’aggiornamento del DOM in un Effetto, lasci che React aggiorni prima lo schermo. Poi il tuo Effetto viene eseguito.

Quando il tuo componente VideoPlayer viene renderizzato (la prima volta o se viene ri-renderizzato), accadono alcune cose. Per prima cosa, React aggiornerà lo schermo, assicurandosi che il tag <video> sia nel DOM con le props corrette. Poi React eseguirà il tuo Effetto. Infine, il tuo Effetto chiamerà play() o pause() a seconda del valore di isPlaying.

Premi Play/Pause più volte e osserva come il video player resta sincronizzato con il valore di isPlaying:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

In questo esempio, il “sistema esterno” con cui ti sei sincronizzato allo state di React era l’API media del browser. Puoi usare un approccio simile per avvolgere codice legacy non-React (come plugin jQuery) in componenti React dichiarativi.

Nota che controllare un video player è molto più complesso in pratica. Chiamare play() può fallire, l’utente potrebbe riprodurre o mettere in pausa usando i controlli integrati del browser e così via. Questo esempio è molto semplificato e incompleto.

Insidia

Per impostazione predefinita, gli Effetti girano dopo ogni renderizzazione. Ecco perché codice come questo produce un loop infinito:

const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});

Gli Effetti girano come risultato della renderizzazione. Impostare lo state avvia la renderizzazione. Impostare lo state immediatamente in un Effetto è come collegare una presa elettrica a se stessa. L’Effetto gira, imposta lo state, il che causa una ri-renderizzazione, il che fa girare l’Effetto, imposta di nuovo lo state, il che causa un’altra ri-renderizzazione, e così via.

Gli Effetti di solito dovrebbero sincronizzare i tuoi componenti con un sistema esterno. Se non c’è un sistema esterno e vuoi solo regolare dello state in base ad altro state, potresti non avere bisogno di un Effetto.

Passo 2: Specificare le dipendenze dell’Effetto

Per impostazione predefinita, gli Effetti girano dopo ogni renderizzazione. Spesso, questo non è quello che vuoi:

  • A volte è lento. Sincronizzarsi con un sistema esterno non è sempre istantaneo, quindi potresti voler saltare l’operazione a meno che non sia necessaria. Per esempio, non vuoi riconnetterti al server di chat a ogni tasto premuto.
  • A volte è sbagliato. Per esempio, non vuoi attivare un’animazione fade-in del componente a ogni tasto premuto. L’animazione dovrebbe riprodursi solo una volta quando il componente appare per la prima volta.

Per dimostrare il problema, ecco l’esempio precedente con alcune chiamate console.log e un input di testo che aggiorna lo state del componente padre. Nota come digitare fa rieseguire l’Effetto:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Puoi dire a React di saltare la riesecuzione non necessaria dell’Effetto specificando un array di dipendenze come secondo argomento della chiamata a useEffect. Inizia aggiungendo un array vuoto [] all’esempio sopra alla riga 14:

useEffect(() => {
// ...
}, []);

Vedrai un errore che dice React Hook useEffect has a missing dependency: 'isPlaying':

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  }, []); // This causes an error

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Il problema è che il codice all’interno del tuo Effetto dipende dalla prop isPlaying per decidere cosa fare, ma questa dipendenza non era stata dichiarata esplicitamente. Per risolvere il problema, aggiungi isPlaying all’array di dipendenze:

useEffect(() => {
if (isPlaying) { // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ...so it must be declared here!

Ora tutte le dipendenze sono dichiarate, quindi non c’è errore. Specificare [isPlaying] come array di dipendenze dice a React di saltare la riesecuzione del tuo Effetto se isPlaying è lo stesso della renderizzazione precedente. Con questa modifica, digitare nell’input non fa rieseguire l’Effetto, ma premere Play/Pause sì:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

L’array di dipendenze può contenere più dipendenze. React salterà la riesecuzione dell’Effetto solo se tutte le dipendenze che specifichi hanno esattamente gli stessi valori della renderizzazione precedente. React confronta i valori delle dipendenze usando il confronto Object.is. Vedi la reference di useEffect per i dettagli.

Nota che non puoi “scegliere” le tue dipendenze. Otterrai un errore del linter se le dipendenze che hai specificato non corrispondono a quelle che React si aspetta in base al codice all’interno del tuo Effetto. Questo aiuta a individuare molti bug nel tuo codice. Se non vuoi che del codice venga rieseguito, modifica il codice dell’Effetto stesso per non “aver bisogno” di quella dipendenza.

Insidia

I comportamenti senza array di dipendenze e con un array di dipendenze vuoto [] sono diversi:

useEffect(() => {
// This runs after every render
});

useEffect(() => {
// This runs only on mount (when the component appears)
}, []);

useEffect(() => {
// This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);

Esamineremo da vicino cosa significa “montare” nel passo successivo.

Approfondimento

Perché il ref è stato omesso dall’array di dipendenze?

Questo Effetto usa sia ref che isPlaying, ma solo isPlaying è dichiarato come dipendenza:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);

Questo perché l’oggetto ref ha un’ identità stabile: React garantisce che otterrai sempre lo stesso oggetto dalla stessa chiamata a useRef a ogni renderizzazione. Non cambia mai, quindi da solo non farà mai rieseguire l’Effetto. Pertanto, non importa se lo includi o meno. Includerlo va bene lo stesso:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);

Anche le funzioni set restituite da useState hanno identità stabile, quindi spesso le vedrai omesse dalle dipendenze. Se il linter ti permette di omettere una dipendenza senza errori, è sicuro farlo.

Omettere dipendenze sempre stabili funziona solo quando il linter può “vedere” che l’oggetto è stabile. Per esempio, se ref fosse passato da un componente padre, dovresti specificarlo nell’array di dipendenze. Tuttavia, questo è positivo perché non puoi sapere se il componente padre passa sempre lo stesso ref o ne passa uno di più condizionalmente. Quindi il tuo Effetto dipenderebbe da quale ref viene passato.

Passo 3: Aggiungere la cleanup se necessario

Considera un esempio diverso. Stai scrivendo un componente ChatRoom che deve connettersi al server di chat quando appare. Ti viene fornita un’API createConnection() che restituisce un oggetto con i metodi connect() e disconnect(). Come mantieni il componente connesso mentre è visualizzato all’utente?

Inizia scrivendo la logica dell’Effetto:

useEffect(() => {
const connection = createConnection();
connection.connect();
});

Sarebbe lento connettersi alla chat dopo ogni ri-renderizzazione, quindi aggiungi l’array di dipendenze:

useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);

Il codice all’interno dell’Effetto non usa props o state, quindi il tuo array di dipendenze è [] (vuoto). Questo dice a React di eseguire questo codice solo quando il componente “monta”, cioè appare sullo schermo per la prima volta.

Proviamo a eseguire questo codice:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

Questo Effetto gira solo al mount, quindi potresti aspettarti che "✅ Connecting..." venga stampato una volta nella console. Tuttavia, se controlli la console, "✅ Connecting..." viene stampato due volte. Perché succede?

Immagina che il componente ChatRoom faccia parte di un’app più grande con molte schermate diverse. L’utente inizia il suo percorso sulla pagina ChatRoom. Il componente monta e chiama connection.connect(). Poi immagina che l’utente navighi verso un’altra schermata — per esempio, la pagina Impostazioni. Il componente ChatRoom smonta. Infine, l’utente clicca Indietro e ChatRoom monta di nuovo. Questo imposterebbe una seconda connessione — ma la prima connessione non è mai stata distrutta! Mentre l’utente naviga nell’app, le connessioni continuerebbero ad accumularsi.

Bug come questo sono facili da perdere senza test manuali estensivi. Per aiutarti a individuarli rapidamente, in modalità di sviluppo React rimonta ogni componente una volta subito dopo il mount iniziale.

Vedere il log "✅ Connecting..." due volte ti aiuta a notare il vero problema: il tuo codice non chiude la connessione quando il componente smonta.

Per risolvere il problema, restituisci una funzione di cleanup dal tuo Effetto:

useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);

React chiamerà la tua funzione di cleanup ogni volta prima che l’Effetto venga rieseguito, e una volta finale quando il componente smonta (viene rimosso). Vediamo cosa succede quando la funzione di cleanup è implementata:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

Ora ottieni tre log in console in modalità di sviluppo:

  1. "✅ Connecting..."
  2. "❌ Disconnected."
  3. "✅ Connecting..."

Questo è il comportamento corretto in modalità di sviluppo. Rimontando il tuo componente, React verifica che navigare via e tornare non rompa il tuo codice. Disconnettersi e poi riconnettersi è esattamente ciò che dovrebbe accadere! Quando implementi bene la cleanup, non dovrebbe esserci alcuna differenza visibile per l’utente tra eseguire l’Effetto una volta rispetto a eseguirlo, ripulirlo ed eseguirlo di nuovo. C’è una coppia extra di connect/disconnect perché React sta testando il tuo codice alla ricerca di bug in modalità di sviluppo. È normale — non cercare di farlo sparire!

In produzione, vedresti "✅ Connecting..." stampato solo una volta. Rimontare i componenti avviene solo in modalità di sviluppo per aiutarti a trovare Effetti che necessitano di cleanup. Puoi disattivare Strict Mode per uscire dal comportamento di sviluppo, ma ti consigliamo di tenerlo attivo. Ti permette di trovare molti bug come quello sopra.

Come gestire l’esecuzione doppia dell’Effetto in modalità di sviluppo?

React rimonta intenzionalmente i tuoi componenti in modalità di sviluppo per trovare bug come nell’ultimo esempio. La domanda giusta non è “come eseguire un Effetto una volta sola”, ma “come correggere il mio Effetto affinché funzioni dopo il rimontaggio”.

Di solito, la risposta è implementare la funzione di cleanup. La funzione di cleanup dovrebbe fermare o annullare ciò che l’Effetto stava facendo. La regola generale è che l’utente non dovrebbe essere in grado di distinguere tra l’Effetto che gira una volta (come in produzione) e una sequenza setup → cleanup → setup (come vedresti in modalità di sviluppo).

La maggior parte degli Effetti che scriverai rientrerà in uno dei pattern comuni sotto.

Insidia

Non usare i ref per impedire l’esecuzione degli Effetti

Un errore comune per impedire agli Effetti di girare due volte in modalità di sviluppo è usare un ref per evitare che l’Effetto giri più di una volta. Per esempio, potresti “correggere” il bug sopra con un useRef:

const connectionRef = useRef(null);
useEffect(() => {
// 🚩 This wont fix the bug!!!
if (!connectionRef.current) {
connectionRef.current = createConnection();
connectionRef.current.connect();
}
}, []);

Questo fa sì che vedi "✅ Connecting..." una sola volta in modalità di sviluppo, ma non corregge il bug.

Quando l’utente naviga via, la connessione non viene ancora chiusa e quando torna indietro, viene creata una nuova connessione. Mentre l’utente naviga nell’app, le connessioni continuerebbero ad accumularsi, come accadeva prima della “correzione”.

Per correggere il bug, non basta far girare l’Effetto una sola volta. L’Effetto deve funzionare dopo il rimontaggio, il che significa che la connessione deve essere ripulita come nella soluzione sopra.

Vedi gli esempi sotto per come gestire i pattern comuni.

Controllare widget non-React

A volte devi aggiungere widget UI non scritti in React. Per esempio, supponiamo che tu stia aggiungendo un componente mappa alla tua pagina. Ha un metodo setZoomLevel() e vorresti mantenere il livello di zoom sincronizzato con una variabile di state zoomLevel nel tuo codice React. Il tuo Effetto assomiglierebbe a questo:

useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);

Nota che in questo caso non serve cleanup. In modalità di sviluppo, React chiamerà l’Effetto due volte, ma non è un problema perché chiamare setZoomLevel due volte con lo stesso valore non fa nulla. Potrebbe essere leggermente più lento, ma non importa perché in produzione non rimonterà inutilmente.

Alcune API potrebbero non permetterti di chiamarle due volte di seguito. Per esempio, il metodo showModal dell’elemento <dialog> integrato lancia un’eccezione se lo chiami due volte. Implementa la funzione di cleanup e falla chiudere il dialog:

useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);

In modalità di sviluppo, il tuo Effetto chiamerà showModal(), poi immediatamente close(), e poi di nuovo showModal(). Questo ha lo stesso comportamento visibile per l’utente di chiamare showModal() una volta, come vedresti in produzione.

Sottoscriversi a eventi

Se il tuo Effetto si sottoscrive a qualcosa, la funzione di cleanup dovrebbe annullare la sottoscrizione:

useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);

In modalità di sviluppo, il tuo Effetto chiamerà addEventListener(), poi immediatamente removeEventListener(), e poi di nuovo addEventListener() con lo stesso handler. Quindi ci sarebbe solo una sottoscrizione attiva alla volta. Questo ha lo stesso comportamento visibile per l’utente di chiamare addEventListener() una volta, come in produzione.

Attivare animazioni

Se il tuo Effetto anima qualcosa in entrata, la funzione di cleanup dovrebbe reimpostare l’animazione ai valori iniziali:

useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);

In modalità di sviluppo, l’opacità sarà impostata a 1, poi a 0, e poi di nuovo a 1. Questo dovrebbe avere lo stesso comportamento visibile per l’utente di impostarla direttamente a 1, che è ciò che accadrebbe in produzione. Se usi una libreria di animazione di terze parti con supporto per il tweening, la tua funzione di cleanup dovrebbe reimpostare la timeline al suo state iniziale.

Recuperare dati

Se il tuo Effetto recupera qualcosa, la funzione di cleanup dovrebbe abortire il fetch o ignorarne il risultato:

useEffect(() => {
let ignore = false;

async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}

startFetching();

return () => {
ignore = true;
};
}, [userId]);

Non puoi “annullare” una richiesta di rete già avvenuta, ma la tua funzione di cleanup dovrebbe assicurarsi che il fetch che non è più rilevante non continui ad influenzare la tua applicazione. Se userId cambia da 'Alice' a 'Bob', la cleanup assicura che la risposta di 'Alice' venga ignorata anche se arriva dopo 'Bob'.

In modalità di sviluppo, vedrai due fetch nella scheda Network. Non c’è nulla di sbagliato. Con l’approccio sopra, il primo Effetto viene immediatamente ripulito quindi la sua copia della variabile ignore viene impostata a true. Quindi, anche se c’è una richiesta extra, non influenzerà lo state grazie al controllo if (!ignore).

In produzione, ci sarà solo una richiesta. Se la seconda richiesta in modalità di sviluppo ti dà fastidio, l’approccio migliore è usare una soluzione che deduplica le richieste e mette in cache le risposte tra i componenti:

function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...

Questo non solo migliorerà l’esperienza di sviluppo, ma renderà anche la tua applicazione più veloce. Per esempio, l’utente che preme il pulsante Indietro non dovrà aspettare che i dati vengano caricati di nuovo perché saranno in cache. Puoi costruire tu stesso una cache del genere o usare una delle molte alternative al fetch manuale negli Effetti.

Approfondimento

Quali sono buone alternative al recupero dati negli Effetti?

Scrivere chiamate fetch all’interno degli Effetti è un modo popolare per recuperare dati, specialmente nelle app completamente client-side. Tuttavia, è un approccio molto manuale e ha svantaggi significativi:

  • Gli Effetti non girano sul server. Questo significa che l’HTML renderizzato inizialmente dal server conterrà solo uno stato di caricamento senza dati. Il computer client dovrà scaricare tutto il JavaScript e renderizzare la tua app solo per scoprire che ora deve caricare i dati. Non è molto efficiente.
  • Recuperare direttamente negli Effetti rende facile creare “network waterfall”. Renderizzi il componente padre, recupera dei dati, renderizza i componenti figli, e poi iniziano a recuperare i loro dati. Se la rete non è molto veloce, questo è significativamente più lento rispetto a recuperare tutti i dati in parallelo.
  • Recuperare direttamente negli Effetti di solito significa che non precarichi o metti in cache i dati. Per esempio, se il componente smonta e poi monta di nuovo, dovrebbe recuperare i dati di nuovo.
  • Non è molto ergonomico. C’è parecchio codice boilerplate quando scrivi chiamate fetch in modo che non soffra di bug come le race condition.

Questo elenco di svantaggi non è specifico di React. Si applica al recupero dati al mount con qualsiasi libreria. Come per il routing, il recupero dati non è banale da fare bene, quindi consigliamo i seguenti approcci:

  • Se usi un framework, usa il suo meccanismo di recupero dati integrato. I framework React moderni hanno meccanismi di recupero dati integrati che sono efficienti e non soffrono dei problemi sopra.
  • Altrimenti, considera di usare o costruire una cache client-side. Soluzioni open source popolari includono TanStack Query, useSWR e React Router 6.4+. Puoi costruire anche la tua soluzione, nel qual caso useresti gli Effetti sotto il cofano, ma aggiungeresti logica per deduplicare le richieste, mettere in cache le risposte ed evitare network waterfall (precaricando i dati o spostando i requisiti di dati alle route).

Puoi continuare a recuperare dati direttamente negli Effetti se nessuno di questi approcci ti soddisfa.

Inviare analytics

Considera questo codice che invia un evento analytics alla visita della pagina:

useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);

In modalità di sviluppo, logVisit verrà chiamato due volte per ogni URL, quindi potresti essere tentato di provare a correggerlo. Ti consigliamo di lasciare questo codice com’è. Come negli esempi precedenti, non c’è alcuna differenza di comportamento visibile per l’utente tra eseguirlo una volta ed eseguirlo due volte. Da un punto di vista pratico, logVisit non dovrebbe fare nulla in modalità di sviluppo perché non vuoi che i log dalle macchine di sviluppo alterino le metriche di produzione. Il tuo componente rimonta ogni volta che salvi il suo file, quindi registra comunque visite extra in modalità di sviluppo.

In produzione, non ci saranno log di visita duplicati.

Per fare debug degli eventi analytics che invii, puoi distribuire la tua app in un ambiente di staging (che gira in modalità produzione) o disattivare temporaneamente Strict Mode e i suoi controlli di rimontaggio solo per lo sviluppo. Puoi anche inviare analytics dai gestori di eventi di cambio route invece che dagli Effetti. Per analytics più precise, gli intersection observer possono aiutare a tracciare quali componenti sono nel viewport e per quanto tempo restano visibili.

Non è un Effetto: Inizializzare l’applicazione

Alcune logiche dovrebbero girare solo una volta all’avvio dell’applicazione. Puoi metterle fuori dai tuoi componenti:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

Questo garantisce che tali logiche girino solo una volta dopo che il browser carica la pagina.

Non è un Effetto: Acquistare un prodotto

A volte, anche se scrivi una funzione di cleanup, non c’è modo di prevenire le conseguenze visibili per l’utente dell’esecuzione dell’Effetto due volte. Per esempio, forse il tuo Effetto invia una richiesta POST come l’acquisto di un prodotto:

useEffect(() => {
// 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);

Non vorresti acquistare il prodotto due volte. Tuttavia, è anche il motivo per cui non dovresti mettere questa logica in un Effetto. Cosa succede se l’utente va su un’altra pagina e poi preme Indietro? Il tuo Effetto girerebbe di nuovo. Non vuoi acquistare il prodotto quando l’utente visita una pagina; vuoi acquistarlo quando l’utente clicca il pulsante Acquista.

L’acquisto non è causato dalla renderizzazione; è causato da un’interazione specifica. Dovrebbe girare solo quando l’utente preme il pulsante. Elimina l’Effetto e sposta la tua richiesta /api/buy nel gestore di eventi del pulsante Acquista:

function handleClick() {
// ✅ Buying is an event because it is caused by a particular interaction.
fetch('/api/buy', { method: 'POST' });
}

Questo illustra che se il rimontaggio rompe la logica della tua applicazione, di solito mette in luce bug esistenti. Dal punto di vista dell’utente, visitare una pagina non dovrebbe essere diverso dal visitarla, cliccare un link e poi premere Indietro per visualizzarla di nuovo. React verifica che i tuoi componenti rispettino questo principio rimontandoli una volta in modalità di sviluppo.

Mettere tutto insieme

Questo playground può aiutarti a “farti un’idea” di come funzionano gli Effetti in pratica.

Questo esempio usa setTimeout per pianificare un log in console con il testo dell’input che appare tre secondi dopo l’esecuzione dell’Effetto. La funzione di cleanup annulla il timeout in sospeso. Inizia premendo “Mount the component”:

import { useState, useEffect } from 'react';

function Playground() {
  const [text, setText] = useState('a');

  useEffect(() => {
    function onTimeout() {
      console.log('⏰ ' + text);
    }

    console.log('🔵 Schedule "' + text + '" log');
    const timeoutId = setTimeout(onTimeout, 3000);

    return () => {
      console.log('🟡 Cancel "' + text + '" log');
      clearTimeout(timeoutId);
    };
  }, [text]);

  return (
    <>
      <label>
        What to log:{' '}
        <input
          value={text}
          onChange={e => setText(e.target.value)}
        />
      </label>
      <h1>{text}</h1>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Unmount' : 'Mount'} the component
      </button>
      {show && <hr />}
      {show && <Playground />}
    </>
  );
}

Vedrai tre log all’inizio: Schedule "a" log, Cancel "a" log e di nuovo Schedule "a" log. Tre secondi dopo ci sarà anche un log che dice a. Come hai imparato prima, la coppia extra di schedule/cancel è perché React rimonta il componente una volta in modalità di sviluppo per verificare che tu abbia implementato bene la cleanup.

Ora modifica l’input per dire abc. Se lo fai abbastanza velocemente, vedrai Schedule "ab" log seguito immediatamente da Cancel "ab" log e Schedule "abc" log. React ripulisce sempre l’Effetto della renderizzazione precedente prima dell’Effetto della renderizzazione successiva. Ecco perché, anche se digiti velocemente nell’input, c’è al massimo un timeout pianificato alla volta. Modifica l’input più volte e osserva la console per farti un’idea di come vengono ripuliti gli Effetti.

Digita qualcosa nell’input e poi premi immediatamente “Unmount the component”. Nota come lo smontaggio ripulisce l’Effetto dell’ultima renderizzazione. Qui, cancella l’ultimo timeout prima che abbia la possibilità di scattare.

Infine, modifica il componente sopra e commenta la funzione di cleanup così che i timeout non vengano cancellati. Prova a digitare abcde velocemente. Cosa ti aspetti che succeda tra tre secondi? console.log(text) all’interno del timeout stamperà l’ultimo text e produrrà cinque log abcde? Provalo per verificare la tua intuizione!

Tre secondi dopo, dovresti vedere una sequenza di log (a, ab, abc, abcd e abcde) invece di cinque log abcde. Ogni Effetto “cattura” il valore di text dalla sua renderizzazione corrispondente. Non importa che lo state text sia cambiato: un Effetto dalla renderizzazione con text = 'ab' vedrà sempre 'ab'. In altre parole, gli Effetti di ogni renderizzazione sono isolati l’uno dall’altro. Se ti chiedi come funziona, puoi leggere delle closure.

Approfondimento

Ogni renderizzazione ha i suoi Effetti

Puoi pensare a useEffect come “attaccare” un pezzo di comportamento all’output della renderizzazione. Considera questo Effetto:

export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);

return <h1>Welcome to {roomId}!</h1>;
}

Vediamo cosa succede esattamente mentre l’utente naviga nell’app.

Renderizzazione iniziale

L’utente visita <ChatRoom roomId="general" />. Sostituiamo mentalmente roomId con 'general':

// JSX for the first render (roomId = "general")
return <h1>Welcome to general!</h1>;

L’Effetto è anche parte dell’output della renderizzazione. L’Effetto della prima renderizzazione diventa:

// Effect for the first render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the first render (roomId = "general")
['general']

React esegue questo Effetto, che si connette alla chat room 'general'.

Ri-renderizzazione con le stesse dipendenze

Supponiamo che <ChatRoom roomId="general" /> venga ri-renderizzato. L’output JSX è lo stesso:

// JSX for the second render (roomId = "general")
return <h1>Welcome to general!</h1>;

React vede che l’output della renderizzazione non è cambiato, quindi non aggiorna il DOM.

L’Effetto della seconda renderizzazione assomiglia a questo:

// Effect for the second render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the second render (roomId = "general")
['general']

React confronta ['general'] della seconda renderizzazione con ['general'] della prima renderizzazione. Poiché tutte le dipendenze sono le stesse, React ignora l’Effetto della seconda renderizzazione. Non viene mai chiamato.

Ri-renderizzazione con dipendenze diverse

Poi, l’utente visita <ChatRoom roomId="travel" />. Questa volta, il componente restituisce JSX diverso:

// JSX for the third render (roomId = "travel")
return <h1>Welcome to travel!</h1>;

React aggiorna il DOM cambiando "Welcome to general" in "Welcome to travel".

L’Effetto della terza renderizzazione assomiglia a questo:

// Effect for the third render (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the third render (roomId = "travel")
['travel']

React confronta ['travel'] della terza renderizzazione con ['general'] della seconda renderizzazione. Una dipendenza è diversa: Object.is('travel', 'general') è false. L’Effetto non può essere saltato.

Prima che React possa applicare l’Effetto della terza renderizzazione, deve ripulire l’ultimo Effetto che è stato eseguito. L’Effetto della seconda renderizzazione è stato saltato, quindi React deve ripulire l’Effetto della prima renderizzazione. Se scorri verso l’alto alla prima renderizzazione, vedrai che la sua cleanup chiama disconnect() sulla connessione creata con createConnection('general'). Questo disconnette l’app dalla chat room 'general'.

Dopo di ciò, React esegue l’Effetto della terza renderizzazione. Si connette alla chat room 'travel'.

Smontaggio

Infine, supponiamo che l’utente navighi via e il componente ChatRoom smonti. React esegue la funzione di cleanup dell’ultimo Effetto. L’ultimo Effetto era della terza renderizzazione. La cleanup della terza renderizzazione distrugge la connessione createConnection('travel'). Quindi l’app si disconnette dalla room 'travel'.

Comportamenti solo di sviluppo

Quando Strict Mode è attivo, React rimonta ogni componente una volta dopo il mount (state e DOM vengono preservati). Questo ti aiuta a trovare Effetti che necessitano di cleanup ed espone precocemente bug come le race condition. Inoltre, React rimonterà gli Effetti ogni volta che salvi un file in modalità di sviluppo. Entrambi questi comportamenti sono solo di sviluppo.

Riepilogo

  • A differenza degli eventi, gli Effetti sono causati dalla renderizzazione stessa piuttosto che da un’interazione particolare.
  • Gli Effetti ti permettono di sincronizzare un componente con un sistema esterno (API di terze parti, rete, ecc.).
  • Per impostazione predefinita, gli Effetti girano dopo ogni renderizzazione (inclusa quella iniziale).
  • React salterà l’Effetto se tutte le sue dipendenze hanno gli stessi valori della renderizzazione precedente.
  • Non puoi “scegliere” le tue dipendenze. Sono determinate dal codice all’interno dell’Effetto.
  • Un array di dipendenze vuoto ([]) corrisponde al “mount” del componente, cioè all’aggiunta sullo schermo.
  • In Strict Mode, React monta i componenti due volte (solo in modalità di sviluppo!) per testare i tuoi Effetti.
  • Se il tuo Effetto si rompe a causa del rimontaggio, devi implementare una funzione di cleanup.
  • React chiamerà la tua funzione di cleanup prima che l’Effetto venga rieseguito la volta successiva, e durante lo smontaggio.

Sfida 1 di 4:
Mettere a fuoco un campo al mount

In questo esempio, il form renderizza un componente <MyInput />.

Usa il metodo focus() dell’input per far sì che MyInput metta automaticamente a fuoco il campo quando appare sullo schermo. C’è già un’implementazione commentata, ma non funziona del tutto. Scopri perché non funziona e correggila. (Se conosci l’attributo autoFocus, fingi che non esista: stiamo reimplementando la stessa funzionalità da zero.)

import { useEffect, useRef } from 'react';

export default function MyInput({ value, onChange }) {
  const ref = useRef(null);

  // TODO: This doesn't quite work. Fix it.
  // ref.current.focus()

  return (
    <input
      ref={ref}
      value={value}
      onChange={onChange}
    />
  );
}

Per verificare che la tua soluzione funzioni, premi “Show form” e verifica che l’input riceva il focus (viene evidenziato e il cursore viene posizionato all’interno). Premi “Hide form” e di nuovo “Show form”. Verifica che l’input venga evidenziato di nuovo.

MyInput dovrebbe mettere a fuoco solo al mount invece che dopo ogni renderizzazione. Per verificare che il comportamento sia corretto, premi “Show form” e poi premi ripetutamente la checkbox “Make it uppercase”. Cliccare la checkbox non dovrebbe mettere a fuoco l’input sopra.