Ciclo di vita degli Effetti reattivi

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.

Gli Effetti hanno un ciclo di vita diverso dai componenti. I componenti possono montare, aggiornarsi o smontarsi. Un Effetto può fare solo due cose: avviare la sincronizzazione di qualcosa e, in seguito, fermarla. Questo ciclo può ripetersi più volte se il tuo Effetto dipende da props e state che cambiano nel tempo. React fornisce una regola del linter per verificare che tu abbia specificato correttamente le dipendenze del tuo Effetto. Questo mantiene il tuo Effetto sincronizzato con le props e lo state più recenti.

Imparerai

  • In che modo il ciclo di vita di un Effetto è diverso dal ciclo di vita di un componente
  • Come pensare a ciascun singolo Effetto in isolamento
  • Quando il tuo Effetto deve re-sincronizzarsi e perché
  • Come vengono determinate le dipendenze del tuo Effetto
  • Cosa significa che un valore è reattivo
  • Cosa significa un array di dipendenze vuoto
  • Come React verifica che le tue dipendenze siano corrette con un linter
  • Cosa fare quando non sei d’accordo con il linter

Il ciclo di vita di un Effetto

Ogni componente React attraversa lo stesso ciclo di vita:

  • Un componente monta quando viene aggiunto allo schermo.
  • Un componente si aggiorna quando riceve nuove props o state, di solito in risposta a un’interazione.
  • Un componente smonta quando viene rimosso dallo schermo.

È un buon modo di pensare ai componenti, ma non agli Effetti. Invece, prova a pensare a ciascun Effetto indipendentemente dal ciclo di vita del tuo componente. Un Effetto descrive come sincronizzare un sistema esterno con le props e lo state attuali. Man mano che il tuo codice cambia, la sincronizzazione dovrà avvenire più o meno spesso.

Per illustrare questo punto, considera questo Effetto che connette il tuo componente a un server di chat:

const serverUrl = 'https://localhost:1234';

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

Il corpo del tuo Effetto specifica come avviare la sincronizzazione:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

La funzione di cleanup restituita dal tuo Effetto specifica come fermare la sincronizzazione:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

Intuitivamente, potresti pensare che React avvii la sincronizzazione quando il tuo componente monta e la fermi quando il tuo componente smonta. Tuttavia, non è tutto qui! A volte può essere necessario avviare e fermare la sincronizzazione più volte mentre il componente resta montato.

Vediamo perché è necessario, quando succede e come puoi controllare questo comportamento.

Nota bene

Alcuni Effetti non restituiscono affatto una funzione di cleanup. Nella maggior parte dei casi, vorrai restituirne una — ma se non lo fai, React si comporterà come se avessi restituito una funzione di cleanup vuota.

Perché la sincronizzazione può dover avvenire più di una volta

Immagina che questo componente ChatRoom riceva una prop roomId che l’utente seleziona in un menu a tendina. Supponiamo che inizialmente l’utente scelga la stanza "general" come roomId. La tua app mostra la chat room "general":

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Dopo che l’UI è stata visualizzata, React eseguirà il tuo Effetto per avviare la sincronizzazione. Si connette alla stanza "general":

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

Fin qui, tutto bene.

In seguito, l’utente sceglie una stanza diversa nel menu a tendina (per esempio, "travel"). Per prima cosa, React aggiornerà l’UI:

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Pensa a cosa dovrebbe succedere dopo. L’utente vede che "travel" è la chat room selezionata nell’UI. Tuttavia, l’Effetto eseguito l’ultima volta è ancora connesso alla stanza "general". La prop roomId è cambiata, quindi ciò che il tuo Effetto ha fatto in precedenza (connettersi alla stanza "general") non corrisponde più all’UI.

A questo punto, vuoi che React faccia due cose:

  1. Fermare la sincronizzazione con il vecchio roomId (disconnettersi dalla stanza "general")
  2. Avviare la sincronizzazione con il nuovo roomId (connettersi alla stanza "travel")

Per fortuna, hai già insegnato a React come fare entrambe le cose! Il corpo del tuo Effetto specifica come avviare la sincronizzazione e la tua funzione di cleanup specifica come fermarla. Tutto ciò che React deve fare ora è chiamarle nell’ordine corretto e con le props e lo state corretti. Vediamo esattamente come avviene.

Come React re-sincronizza il tuo Effetto

Ricorda che il tuo componente ChatRoom ha ricevuto un nuovo valore per la prop roomId. Prima era "general", ora è "travel". React deve re-sincronizzare il tuo Effetto per riconnetterti a una stanza diversa.

Per fermare la sincronizzazione, React chiamerà la funzione di cleanup che il tuo Effetto ha restituito dopo essersi connesso alla stanza "general". Poiché roomId era "general", la funzione di cleanup si disconnette dalla stanza "general":

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

Poi React eseguirà l’Effetto che hai fornito durante questa renderizzazione. Questa volta, roomId è "travel", quindi avvierà la sincronizzazione con la chat room "travel" (finché anche la sua funzione di cleanup non verrà chiamata):

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

Grazie a questo, ora sei connesso alla stessa stanza che l’utente ha scelto nell’UI. Disastro evitato!

Ogni volta che il tuo componente si ri-renderizza con un roomId diverso, il tuo Effetto si re-sincronizzerà. Per esempio, supponiamo che l’utente cambi roomId da "travel" a "music". React fermerà di nuovo la sincronizzazione del tuo Effetto chiamando la sua funzione di cleanup (disconnettendoti dalla stanza "travel"). Poi riavvierà la sincronizzazione eseguendo il suo corpo con la nuova prop roomId (connettendoti alla stanza "music").

Infine, quando l’utente va a una schermata diversa, ChatRoom smonta. Ora non c’è più bisogno di restare connessi. React fermerà la sincronizzazione del tuo Effetto un’ultima volta e ti disconnetterà dalla chat room "music".

Pensare dal punto di vista dell’Effetto

Ricapitoliamo tutto ciò che è successo dal punto di vista del componente ChatRoom:

  1. ChatRoom monta con roomId impostato su "general"
  2. ChatRoom si aggiorna con roomId impostato su "travel"
  3. ChatRoom si aggiorna con roomId impostato su "music"
  4. ChatRoom smonta

Durante ciascuno di questi punti del ciclo di vita del componente, il tuo Effetto ha fatto cose diverse:

  1. Il tuo Effetto si è connesso alla stanza "general"
  2. Il tuo Effetto si è disconnesso dalla stanza "general" e si è connesso alla stanza "travel"
  3. Il tuo Effetto si è disconnesso dalla stanza "travel" e si è connesso alla stanza "music"
  4. Il tuo Effetto si è disconnesso dalla stanza "music"

Ora pensiamo a cosa è successo dal punto di vista dell’Effetto stesso:

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

La struttura di questo codice potrebbe ispirarti a vedere ciò che è successo come una sequenza di periodi di tempo non sovrapposti:

  1. Il tuo Effetto si è connesso alla stanza "general" (finché non si è disconnesso)
  2. Il tuo Effetto si è connesso alla stanza "travel" (finché non si è disconnesso)
  3. Il tuo Effetto si è connesso alla stanza "music" (finché non si è disconnesso)

In precedenza, pensavi dal punto di vista del componente. Quando guardavi dal punto di vista del componente, era tentante pensare agli Effetti come “callback” o “eventi del ciclo di vita” che scattano in un momento specifico come “dopo una renderizzazione” o “prima dello smontaggio”. Questo modo di pensare diventa complicato molto rapidamente, quindi è meglio evitarlo.

Invece, concentrati sempre su un singolo ciclo di avvio/arresto alla volta. Non dovrebbe importare se un componente sta montando, aggiornandosi o smontando. Tutto ciò che devi fare è descrivere come avviare la sincronizzazione e come fermarla. Se lo fai bene, il tuo Effetto sarà resiliente all’essere avviato e fermato quante volte è necessario.

Questo potrebbe ricordarti come non pensi se un componente sta montando o aggiornandosi quando scrivi la logica di renderizzazione che crea il JSX. Descrivi cosa dovrebbe esserci sullo schermo e React fa il resto.

Come React verifica che il tuo Effetto possa re-sincronizzarsi

Ecco un esempio interattivo con cui puoi giocare. Premi “Open chat” per montare il componente ChatRoom:

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

const serverUrl = 'https://localhost:1234';

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

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Nota che quando il componente monta per la prima volta, vedi tre log:

  1. ✅ Connecting to "general" room at https://localhost:1234... (solo in sviluppo)
  2. ❌ Disconnected from "general" room at https://localhost:1234. (solo in sviluppo)
  3. ✅ Connecting to "general" room at https://localhost:1234...

I primi due log sono solo in sviluppo. In sviluppo, React rimonta sempre ogni componente una volta.

React verifica che il tuo Effetto possa re-sincronizzarsi forzandolo a farlo immediatamente in sviluppo. Questo potrebbe ricordarti l’apertura di una porta e la sua chiusura un’ulteriore volta per verificare se la serratura funziona. React avvia e ferma il tuo Effetto un’ulteriore volta in sviluppo per verificare che tu abbia implementato bene la sua cleanup.

Il motivo principale per cui il tuo Effetto si re-sincronizzerà in pratica è se sono cambiati alcuni dati che usa. Nella sandbox sopra, cambia la chat room selezionata. Nota come, quando roomId cambia, il tuo Effetto si re-sincronizza.

Tuttavia, ci sono anche casi più insoliti in cui la re-sincronizzazione è necessaria. Per esempio, prova a modificare serverUrl nella sandbox sopra mentre la chat è aperta. Nota come l’Effetto si re-sincronizza in risposta alle tue modifiche al codice. In futuro, React potrebbe aggiungere altre funzionalità che si basano sulla re-sincronizzazione.

Come React sa che deve re-sincronizzare l’Effetto

Potresti chiederti come React ha saputo che il tuo Effetto doveva re-sincronizzarsi dopo il cambio di roomId. È perché hai detto a React che il suo codice dipende da roomId includendolo nell’elenco delle dipendenze:

function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...

Ecco come funziona:

  1. Sapevi che roomId è una prop, il che significa che può cambiare nel tempo.
  2. Sapevi che il tuo Effetto legge roomId (quindi la sua logica dipende da un valore che potrebbe cambiare in seguito).
  3. Per questo l’hai specificato come dipendenza del tuo Effetto (in modo che si re-sincronizzi quando roomId cambia).

Ogni volta che il tuo componente si ri-renderizza, React esaminerà l’array di dipendenze che hai passato. Se uno qualsiasi dei valori nell’array è diverso dal valore nella stessa posizione che hai passato durante la renderizzazione precedente, React re-sincronizzerà il tuo Effetto.

Per esempio, se hai passato ["general"] durante la renderizzazione iniziale e in seguito hai passato ["travel"] durante la renderizzazione successiva, React confronterà "general" e "travel". Questi sono valori diversi (confrontati con Object.is), quindi React re-sincronizzerà il tuo Effetto. D’altra parte, se il tuo componente si ri-renderizza ma roomId non è cambiato, il tuo Effetto resterà connesso alla stessa stanza.

Ogni Effetto rappresenta un processo di sincronizzazione separato

Resisti all’impulso di aggiungere logica non correlata al tuo Effetto solo perché questa logica deve girare nello stesso momento di un Effetto che hai già scritto. Per esempio, supponiamo che tu voglia inviare un evento analytics quando l’utente visita la stanza. Hai già un Effetto che dipende da roomId, quindi potresti sentirti tentato di aggiungere lì la chiamata analytics:

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

Ma immagina di aggiungere in seguito un’altra dipendenza a questo Effetto che necessita di ristabilire la connessione. Se questo Effetto si re-sincronizza, chiamerà anche logVisit(roomId) per la stessa stanza, cosa che non intendevi. Registrare la visita è un processo separato dalla connessione. Scrivili come due Effetti separati:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

Ogni Effetto nel tuo codice dovrebbe rappresentare un processo di sincronizzazione separato e indipendente.

Nell’esempio sopra, eliminare un Effetto non romperebbe la logica dell’altro Effetto. Questa è una buona indicazione che sincronizzano cose diverse, quindi aveva senso separarli. D’altra parte, se dividi un pezzo coeso di logica in Effetti separati, il codice potrebbe sembrare “più pulito” ma sarà più difficile da mantenere. Per questo dovresti pensare se i processi sono gli stessi o separati, non se il codice sembra più pulito.

Gli Effetti “reagiscono” ai valori reattivi

Il tuo Effetto legge due variabili (serverUrl e roomId), ma hai specificato solo roomId come dipendenza:

const serverUrl = 'https://localhost:1234';

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

Perché serverUrl non ha bisogno di essere una dipendenza?

Questo perché serverUrl non cambia mai a causa di una ri-renderizzazione. È sempre lo stesso indipendentemente da quante volte il componente si ri-renderizza e perché. Poiché serverUrl non cambia mai, non avrebbe senso specificarlo come dipendenza. Dopotutto, le dipendenze fanno qualcosa solo quando cambiano nel tempo!

D’altra parte, roomId potrebbe essere diverso in una ri-renderizzazione. Props, state e altri valori dichiarati all’interno del componente sono reattivi perché vengono calcolati durante la renderizzazione e partecipano al flusso di dati di React.

Se serverUrl fosse una variabile di state, sarebbe reattivo. I valori reattivi devono essere inclusi nelle dipendenze:

function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}

Includendo serverUrl come dipendenza, ti assicuri che l’Effetto si re-sincronizzi dopo che cambia.

Prova a cambiare la chat room selezionata o a modificare l’URL del server in questa sandbox:

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Ogni volta che cambi un valore reattivo come roomId o serverUrl, l’Effetto si riconnette al server di chat.

Cosa significa un Effetto con dipendenze vuote

Cosa succede se sposti sia serverUrl che roomId fuori dal componente?

const serverUrl = 'https://localhost:1234';
const roomId = 'general';

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Ora il codice del tuo Effetto non usa alcun valore reattivo, quindi le sue dipendenze possono essere vuote ([]).

Pensando dal punto di vista del componente, l’array di dipendenze vuoto [] significa che questo Effetto si connette alla chat room solo quando il componente monta e si disconnette solo quando il componente smonta. (Tieni presente che React lo re-sincronizzerebbe comunque un’ulteriore volta in sviluppo per mettere alla prova la tua logica.)

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

const serverUrl = 'https://localhost:1234';
const roomId = 'general';

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

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

Tuttavia, se pensi dal punto di vista dell’Effetto, non hai bisogno di pensare affatto a montaggio e smontaggio. Ciò che è importante è che tu abbia specificato cosa fa il tuo Effetto per avviare e fermare la sincronizzazione. Oggi non ha dipendenze reattive. Ma se in futuro vorrai che l’utente cambi roomId o serverUrl nel tempo (e diventerebbero reattivi), il codice del tuo Effetto non cambierà. Dovrai solo aggiungerli alle dipendenze.

Tutte le variabili dichiarate nel corpo del componente sono reattive

Props e state non sono gli unici valori reattivi. Anche i valori che calcoli a partire da essi sono reattivi. Se le props o lo state cambiano, il tuo componente si ri-renderizzerà e anche i valori calcolati a partire da essi cambieranno. Per questo tutte le variabili del corpo del componente usate dall’Effetto dovrebbero essere nell’elenco delle dipendenze dell’Effetto.

Supponiamo che l’utente possa scegliere un server di chat nel menu a tendina, ma possa anche configurare un server predefinito nelle impostazioni. Supponiamo che tu abbia già messo lo state delle impostazioni in un context così leggi settings da quel context. Ora calcoli serverUrl in base al server selezionato dalle props e al server predefinito:

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

In questo esempio, serverUrl non è una prop né una variabile di state. È una variabile regolare che calcoli durante la renderizzazione. Ma viene calcolata durante la renderizzazione, quindi può cambiare a causa di una ri-renderizzazione. Per questo è reattiva.

Tutti i valori all’interno del componente (incluse props, state e variabili nel corpo del componente) sono reattivi. Qualsiasi valore reattivo può cambiare in una ri-renderizzazione, quindi devi includere i valori reattivi come dipendenze dell’Effetto.

In altre parole, gli Effetti “reagiscono” a tutti i valori del corpo del componente.

Approfondimento

I valori globali o mutabili possono essere dipendenze?

I valori mutabili (incluse le variabili globali) non sono reattivi.

Un valore mutabile come location.pathname non può essere una dipendenza. È mutabile, quindi può cambiare in qualsiasi momento completamente al di fuori del flusso di dati di renderizzazione di React. Cambiarlo non avvierebbe una ri-renderizzazione del tuo componente. Pertanto, anche se lo specificassi nelle dipendenze, React non saprebbe di re-sincronizzare l’Effetto quando cambia. Questo viola anche le regole di React perché leggere dati mutabili durante la renderizzazione (quando calcoli le dipendenze) rompe la purezza della renderizzazione. Invece, dovresti leggere e sottoscriverti a un valore mutabile esterno con useSyncExternalStore.

Un valore mutabile come ref.current o ciò che leggi da esso non può essere una dipendenza. L’oggetto ref restituito da useRef stesso può essere una dipendenza, ma la sua proprietà current è intenzionalmente mutabile. Ti permette di tenere traccia di qualcosa senza avviare una ri-renderizzazione. Ma poiché cambiarlo non avvia una ri-renderizzazione, non è un valore reattivo e React non saprà di rieseguire il tuo Effetto quando cambia.

Come imparerai più avanti in questa pagina, un linter verificherà automaticamente questi problemi.

React verifica che tu abbia specificato ogni valore reattivo come dipendenza

Se il tuo linter è configurato per React, verificherà che ogni valore reattivo usato dal codice del tuo Effetto sia dichiarato come sua dipendenza. Per esempio, questo è un errore del linter perché sia roomId che serverUrl sono reattivi:

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

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Questo potrebbe sembrare un errore di React, ma in realtà React sta segnalando un bug nel tuo codice. Sia roomId che serverUrl possono cambiare nel tempo, ma stai dimenticando di re-sincronizzare il tuo Effetto quando cambiano. Resti connesso al roomId e serverUrl iniziali anche dopo che l’utente ha scelto valori diversi nell’UI.

Per correggere il bug, segui il suggerimento del linter di specificare roomId e serverUrl come dipendenze del tuo Effetto:

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

Prova questa correzione nella sandbox sopra. Verifica che l’errore del linter sia sparito e che la chat si riconnetta quando necessario.

Nota bene

In alcuni casi, React sa che un valore non cambia mai anche se è dichiarato all’interno del componente. Per esempio, la funzione set restituita da useState e l’oggetto ref restituito da useRef sono stabili — è garantito che non cambino in una ri-renderizzazione. I valori stabili non sono reattivi, quindi puoi ometterli dall’elenco. Includerli è consentito: non cambieranno, quindi non importa.

Cosa fare quando non vuoi re-sincronizzare

Nell’esempio precedente, hai corretto l’errore del linter elencando roomId e serverUrl come dipendenze.

Tuttavia, potresti invece “dimostrare” al linter che questi valori non sono valori reattivi, cioè che non possono cambiare a causa di una ri-renderizzazione. Per esempio, se serverUrl e roomId non dipendono dalla renderizzazione e hanno sempre gli stessi valori, puoi spostarli fuori dal componente. Ora non hanno bisogno di essere dipendenze:

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Puoi anche spostarli all’interno dell’Effetto. Non vengono calcolati durante la renderizzazione, quindi non sono reattivi:

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Gli Effetti sono blocchi di codice reattivi. Si re-sincronizzano quando cambiano i valori che leggi al loro interno. A differenza dei gestori di eventi, che girano solo una volta per interazione, gli Effetti girano ogni volta che la sincronizzazione è necessaria.

Non puoi “scegliere” le tue dipendenze. Le tue dipendenze devono includere ogni valore reattivo che leggi nell’Effetto. Il linter lo impone. A volte questo può portare a problemi come loop infiniti e al tuo Effetto che si re-sincronizza troppo spesso. Non correggere questi problemi sopprimendo il linter! Ecco cosa provare invece:

  • Verifica che il tuo Effetto rappresenti un processo di sincronizzazione indipendente. Se il tuo Effetto non sincronizza nulla, potrebbe essere superfluo. Se sincronizza diverse cose indipendenti, dividilo.

  • Se vuoi leggere l’ultimo valore di props o state senza “reagire” ad esso e re-sincronizzare l’Effetto, puoi dividere il tuo Effetto in una parte reattiva (che manterrai nell’Effetto) e una parte non reattiva (che estrarrai in qualcosa chiamato Effect Event). Leggi riguardo alla separazione degli Eventi dagli Effetti.

  • Evita di fare affidamento su oggetti e funzioni come dipendenze. Se crei oggetti e funzioni durante la renderizzazione e poi li leggi da un Effetto, saranno diversi a ogni renderizzazione. Questo farà re-sincronizzare il tuo Effetto ogni volta. Leggi di più sulla rimozione delle dipendenze non necessarie dagli Effetti.

Insidia

Il linter è tuo amico, ma i suoi poteri sono limitati. Il linter sa solo quando le dipendenze sono sbagliate. Non sa il modo migliore per risolvere ogni caso. Se il linter suggerisce una dipendenza, ma aggiungerla causa un loop, non significa che il linter debba essere ignorato. Devi cambiare il codice all’interno (o all’esterno) dell’Effetto in modo che quel valore non sia reattivo e non abbia bisogno di essere una dipendenza.

Se hai una codebase esistente, potresti avere alcuni Effetti che sopprimono il linter così:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Nelle prossime pagine, imparerai come correggere questo codice senza violare le regole. Vale sempre la pena correggerlo!

Riepilogo

  • I componenti possono montare, aggiornarsi e smontare.
  • Ogni Effetto ha un ciclo di vita separato dal componente circostante.
  • Ogni Effetto descrive un processo di sincronizzazione separato che può avviarsi e fermarsi.
  • Quando scrivi e leggi gli Effetti, pensa dal punto di vista di ciascun singolo Effetto (come avviare e fermare la sincronizzazione) piuttosto che dal punto di vista del componente (come monta, si aggiorna o smonta).
  • I valori dichiarati nel corpo del componente sono “reattivi”.
  • I valori reattivi dovrebbero re-sincronizzare l’Effetto perché possono cambiare nel tempo.
  • Il linter verifica che tutti i valori reattivi usati all’interno dell’Effetto siano specificati come dipendenze.
  • Tutti gli errori segnalati dal linter sono legittimi. C’è sempre un modo per correggere il codice senza violare le regole.

Sfida 1 di 5:
Correggere la riconnessione a ogni battitura

In questo esempio, il componente ChatRoom si connette alla chat room quando il componente monta, si disconnette quando smonta e si riconnette quando selezioni una chat room diversa. Questo comportamento è corretto, quindi devi mantenerlo funzionante.

Tuttavia, c’è un problema. Ogni volta che digiti nell’input del messaggio in basso, ChatRoom si riconnette anche alla chat. (Puoi notarlo svuotando la console e digitando nell’input.) Correggi il problema in modo che questo non accada.

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

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

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}