Rimuovere le dipendenze degli 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.

Quando scrivi un Effetto, il linter verifica che tu abbia incluso ogni valore reattivo (come props e state) che l’Effetto legge nell’elenco delle dipendenze del tuo Effetto. Questo garantisce che il tuo Effetto resti sincronizzato con le props e lo state più recenti del tuo componente. Dipendenze non necessarie possono far eseguire il tuo Effetto troppo spesso, o persino creare un loop infinito. Segui questa guida per rivedere e rimuovere le dipendenze non necessarie dai tuoi Effetti.

Imparerai

  • Come correggere loop infiniti di dipendenze degli Effetti
  • Cosa fare quando vuoi rimuovere una dipendenza
  • Come leggere un valore dal tuo Effetto senza “reagire” ad esso
  • Come e perché evitare dipendenze su oggetti e funzioni
  • Perché sopprimere il linter delle dipendenze è pericoloso, e cosa fare invece

Le dipendenze devono corrispondere al codice

Quando scrivi un Effetto, per prima cosa specifichi come avviare e fermare ciò che vuoi che il tuo Effetto faccia:

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

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

Poi, se lasci vuote le dipendenze dell’Effetto ([]), il linter suggerirà le dipendenze corrette:

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();
  }, []); // <-- Fix the mistake here!
  return <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} />
    </>
  );
}

Compilale in base a ciò che dice il linter:

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

Gli Effetti “reagiscono” ai valori reattivi. Poiché roomId è un valore reattivo (può cambiare a causa di una ri-renderizzazione), il linter verifica che tu l’abbia specificato come dipendenza. Se roomId riceve un valore diverso, React re-sincronizzerà il tuo Effetto. Questo garantisce che la chat resti connessa alla stanza selezionata e “reagisca” al menu a tendina:

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');
  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} />
    </>
  );
}

Per rimuovere una dipendenza, dimostra che non lo è

Nota che non puoi “scegliere” le dipendenze del tuo Effetto. Ogni valore reattivo usato dal codice del tuo Effetto deve essere dichiarato nell’elenco delle dipendenze. L’elenco delle dipendenze è determinato dal codice circostante:

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

function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}

I valori reattivi includono le props e tutte le variabili e funzioni dichiarate direttamente all’interno del tuo componente. Poiché roomId è un valore reattivo, non puoi rimuoverlo dall’elenco delle dipendenze. Il linter non lo permetterebbe:

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

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}

E il linter avrebbe ragione! Poiché roomId può cambiare nel tempo, questo introdurrebbe un bug nel tuo codice.

Per rimuovere una dipendenza, “dimostra” al linter che non ha bisogno di essere una dipendenza. Per esempio, puoi spostare roomId fuori dal tuo componente per dimostrare che non è reattivo e non cambierà alle ri-renderizzazioni:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

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

Ora che roomId non è un valore reattivo (e non può cambiare a una ri-renderizzazione), non ha bisogno di essere una dipendenza:

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

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

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

Ecco perché ora puoi specificare un elenco di dipendenze vuoto ([]). Il tuo Effetto davvero non dipende più da alcun valore reattivo, quindi davvero non ha bisogno di rieseguirsi quando cambiano le props o lo state del componente.

Per cambiare le dipendenze, cambia il codice

Potresti aver notato uno schema nel tuo flusso di lavoro:

  1. Per prima cosa, cambi il codice del tuo Effetto o il modo in cui i tuoi valori reattivi sono dichiarati.
  2. Poi, segui il linter e adatti le dipendenze per corrispondere al codice che hai cambiato.
  3. Se non sei soddisfatto dell’elenco delle dipendenze, torni al primo passo (e cambi di nuovo il codice).

L’ultima parte è importante. Se vuoi cambiare le dipendenze, cambia prima il codice circostante. Puoi pensare all’elenco delle dipendenze come un elenco di tutti i valori reattivi usati dal codice del tuo Effetto. Non scegli cosa mettere in quell’elenco. L’elenco descrive il tuo codice. Per cambiare l’elenco delle dipendenze, cambia il codice.

Potrebbe sembrare di risolvere un’equazione. Potresti partire con un obiettivo (per esempio, rimuovere una dipendenza) e devi “trovare” il codice che corrisponde a quell’obiettivo. Non a tutti piace risolvere equazioni, e lo stesso si potrebbe dire della scrittura degli Effetti! Per fortuna, c’è un elenco di ricette comuni che puoi provare qui sotto.

Insidia

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
}, []);

Quando le dipendenze non corrispondono al codice, c’è un rischio molto alto di introdurre bug. Sopprimendo il linter, “menti” a React riguardo ai valori da cui dipende il tuo Effetto.

Usa invece le tecniche qui sotto.

Approfondimento

Perché sopprimere il linter delle dipendenze è così pericoloso?

Sopprimere il linter porta a bug molto poco intuitivi, difficili da trovare e correggere. Ecco un esempio:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Supponiamo che tu voglia eseguire l’Effetto “solo al montaggio”. Hai letto che le dipendenze vuote ([]) fanno questo, quindi hai deciso di ignorare il linter e hai specificato forzatamente [] come dipendenze.

Questo contatore doveva incrementare ogni secondo dell’importo configurabile con i due pulsanti. Tuttavia, poiché hai “mentito” a React che questo Effetto non dipende da nulla, React continua per sempre a usare la funzione onTick dalla renderizzazione iniziale. Durante quella renderizzazione, count era 0 e increment era 1. Ecco perché onTick da quella renderizzazione chiama sempre setCount(0 + 1) ogni secondo, e vedi sempre 1. Bug come questo sono più difficili da correggere quando sono sparsi tra più componenti.

C’è sempre una soluzione migliore che ignorare il linter! Per correggere questo codice, devi aggiungere onTick all’elenco delle dipendenze. (Per garantire che l’intervallo venga configurato una sola volta, rendi onTick un Effect Event.)

Ti consigliamo di trattare l’errore del linter delle dipendenze come un errore di compilazione. Se non lo sopprimi, non vedrai mai bug come questo. Il resto di questa pagina documenta le alternative per questo e altri casi.

Rimuovere le dipendenze non necessarie

Ogni volta che adatti le dipendenze dell’Effetto per riflettere il codice, guarda l’elenco delle dipendenze. Ha senso che l’Effetto si riesegua quando cambia una di queste dipendenze? A volte, la risposta è “no”:

  • Potresti voler rieseguire parti diverse del tuo Effetto in condizioni diverse.
  • Potresti voler leggere solo l’ultimo valore di una dipendenza invece di “reagire” ai suoi cambiamenti.
  • Una dipendenza può cambiare troppo spesso involontariamente perché è un oggetto o una funzione.

Per trovare la soluzione giusta, dovrai rispondere ad alcune domande sul tuo Effetto. Esaminiamole.

Questo codice dovrebbe essere spostato in un gestore di eventi?

La prima cosa a cui dovresti pensare è se questo codice dovrebbe essere un Effetto in assoluto.

Immagina un form. All’invio, imposti la variabile di state submitted su true. Devi inviare una richiesta POST e mostrare una notifica. Hai messo questa logica dentro un Effetto che “reagisce” a submitted che diventa true:

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Più tardi, vuoi stilizzare il messaggio di notifica in base al tema corrente, quindi leggi il tema corrente. Poiché theme è dichiarato nel corpo del componente, è un valore reattivo, quindi lo aggiungi come dipendenza:

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Facendo così, hai introdotto un bug. Immagina di inviare prima il form e poi di passare tra i temi Dark e Light. Il theme cambierà, l’Effetto si rieseguirà e quindi mostrerà di nuovo la stessa notifica!

Il problema qui è che questo non dovrebbe essere un Effetto in primo luogo. Vuoi inviare questa richiesta POST e mostrare la notifica in risposta all’invio del form, che è una particolare interazione. Per eseguire del codice in risposta a una particolare interazione, metti quella logica direttamente nel corrispondente gestore di eventi:

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Buono: la logica specifica dell'evento è chiamata dai gestori di eventi
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

Ora che il codice è in un gestore di eventi, non è reattivo — quindi verrà eseguito solo quando l’utente invia il form. Leggi di più su scegliere tra gestori di eventi ed Effetti e come eliminare Effetti non necessari.

Il tuo Effetto fa diverse cose non correlate?

La prossima domanda che dovresti farti è se il tuo Effetto fa diverse cose non correlate.

Immagina di creare un form di spedizione in cui l’utente deve scegliere la città e l’area. Recuperi l’elenco di cities dal server in base al country selezionato per mostrarle in un menu a tendina:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

Questo è un buon esempio di recupero dati in un Effetto. Stai sincronizzando lo state cities con la rete in base alla prop country. Non puoi farlo in un gestore di eventi perché devi recuperare i dati non appena ShippingForm viene visualizzato e ogni volta che country cambia (indipendentemente da quale interazione lo causa).

Ora supponiamo che tu stia aggiungendo un secondo menu a tendina per le aree della città, che dovrebbe recuperare le areas per la city attualmente selezionata. Potresti iniziare aggiungendo una seconda chiamata fetch per l’elenco delle aree all’interno dello stesso Effetto:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

Tuttavia, poiché l’Effetto ora usa la variabile di state city, hai dovuto aggiungere city all’elenco delle dipendenze. Questo, a sua volta, ha introdotto un problema: quando l’utente seleziona una città diversa, l’Effetto si rieseguirà e chiamerà fetchCities(country). Di conseguenza, recupererai inutilmente l’elenco delle città molte volte.

Il problema con questo codice è che stai sincronizzando due cose diverse e non correlate:

  1. Vuoi sincronizzare lo state cities con la rete in base alla prop country.
  2. Vuoi sincronizzare lo state areas con la rete in base allo state city.

Dividi la logica in due Effetti, ciascuno dei quali reagisce alla prop con cui deve sincronizzarsi:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

Ora il primo Effetto si riesegue solo se country cambia, mentre il secondo Effetto si riesegue quando city cambia. Li hai separati per scopo: due cose diverse sono sincronizzate da due Effetti separati. Due Effetti separati hanno due elenchi di dipendenze separati, quindi non si attiveranno a vicenda involontariamente.

Il codice finale è più lungo dell’originale, ma dividere questi Effetti resta corretto. Ogni Effetto dovrebbe rappresentare un processo di sincronizzazione indipendente. In questo esempio, eliminare un Effetto non rompe la logica dell’altro Effetto. Questo significa che sincronizzano cose diverse, ed è bene dividerli. Se ti preoccupa la duplicazione, puoi migliorare questo codice estraendo la logica ripetitiva in un custom Hook.

Stai leggendo dello state per calcolare il prossimo state?

Questo Effetto aggiorna la variabile di state messages con un array appena creato ogni volta che arriva un nuovo messaggio:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

Usa la variabile messages per creare un nuovo array che inizia con tutti i messaggi esistenti e aggiunge il nuovo messaggio alla fine. Tuttavia, poiché messages è un valore reattivo letto da un Effetto, deve essere una dipendenza:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

E includere messages tra le dipendenze introduce un problema.

Ogni volta che ricevi un messaggio, setMessages() fa ri-renderizzare il componente con un nuovo array messages che include il messaggio ricevuto. Tuttavia, poiché questo Effetto ora dipende da messages, questo re-sincronizzerà anche l’Effetto. Quindi ogni nuovo messaggio farà riconnettere la chat. L’utente non gradirebbe!

Per correggere il problema, non leggere messages all’interno dell’Effetto. Passa invece una funzione updater a setMessages:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Nota come il tuo Effetto non legge affatto la variabile messages ora. Devi solo passare una funzione updater come msgs => [...msgs, receivedMessage]. React mette la tua funzione updater in coda e le fornirà l’argomento msgs durante la prossima renderizzazione. Ecco perché l’Effetto stesso non ha più bisogno di dipendere da messages. Come risultato di questa correzione, ricevere un messaggio in chat non farà più riconnettere la chat.

Vuoi leggere un valore senza “reagire” ai suoi cambiamenti?

Supponiamo che tu voglia riprodurre un suono quando l’utente riceve un nuovo messaggio, a meno che isMuted non sia true:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

Poiché il tuo Effetto ora usa isMuted nel suo codice, devi aggiungerlo alle dipendenze:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

Il problema è che ogni volta che isMuted cambia (per esempio, quando l’utente preme l’interruttore “Muted”), l’Effetto si re-sincronizzerà e si riconnetterà alla chat. Questa non è l’esperienza utente desiderata! (In questo esempio, anche disabilitare il linter non funzionerebbe — se lo fai, isMuted resterebbe “bloccato” con il suo vecchio valore.)

Per risolvere questo problema, devi estrarre la logica che non dovrebbe essere reattiva dall’Effetto. Non vuoi che questo Effetto “reagisca” ai cambiamenti di isMuted. Sposta questo pezzo di logica non reattiva in un Effect Event:

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

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Gli Effect Event ti permettono di dividere un Effetto in parti reattive (che dovrebbero “reagire” a valori reattivi come roomId e ai loro cambiamenti) e parti non reattive (che leggono solo i loro valori più recenti, come onMessage legge isMuted). Ora che leggi isMuted dentro un Effect Event, non ha bisogno di essere una dipendenza del tuo Effetto. Di conseguenza, la chat non si riconnetterà quando attivi e disattivi l’impostazione “Muted”, risolvendo il problema originale!

Avvolgere un gestore di eventi dalle props

Potresti incontrare un problema simile quando il tuo componente riceve un gestore di eventi come prop:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

Supponiamo che il componente genitore passi una funzione onReceiveMessage diversa a ogni renderizzazione:

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

Poiché onReceiveMessage è una dipendenza, farebbe re-sincronizzare l’Effetto dopo ogni ri-renderizzazione del genitore. Questo lo farebbe riconnettere alla chat. Per risolvere, avvolgi la chiamata in un Effect Event:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Gli Effect Event non sono reattivi, quindi non devi specificarli come dipendenze. Di conseguenza, la chat non si riconnetterà più anche se il componente genitore passa una funzione diversa a ogni ri-renderizzazione.

Separare codice reattivo e non reattivo

In questo esempio, vuoi registrare una visita ogni volta che roomId cambia. Vuoi includere l’attuale notificationCount con ogni log, ma non vuoi che un cambiamento di notificationCount attivi un evento di log.

La soluzione è ancora una volta dividere il codice non reattivo in un Effect Event:

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}

Vuoi che la tua logica sia reattiva rispetto a roomId, quindi leggi roomId all’interno del tuo Effetto. Tuttavia, non vuoi che un cambiamento di notificationCount registri una visita extra, quindi leggi notificationCount all’interno dell’Effect Event. Scopri di più sulla lettura delle props e dello state più recenti dagli Effetti usando gli Effect Event.

Qualche valore reattivo cambia involontariamente?

A volte, vuoi che il tuo Effetto “reagisca” a un certo valore, ma quel valore cambia più spesso di quanto vorresti — e potrebbe non riflettere alcun cambiamento reale dal punto di vista dell’utente. Per esempio, supponiamo che tu crei un oggetto options nel corpo del tuo componente, e poi legga quell’oggetto dall’interno del tuo Effetto:

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

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

Questo oggetto è dichiarato nel corpo del componente, quindi è un valore reattivo. Quando leggi un valore reattivo così all’interno di un Effetto, lo dichiari come dipendenza. Questo garantisce che il tuo Effetto “reagisca” ai suoi cambiamenti:

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

È importante dichiararlo come dipendenza! Questo garantisce, per esempio, che se roomId cambia, il tuo Effetto si riconnetterà alla chat con le nuove options. Tuttavia, c’è anche un problema con il codice sopra. Per vederlo, prova a digitare nell’input nella sandbox qui sotto e osserva cosa succede nella console:

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

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

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

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

  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} />
    </>
  );
}

Nella sandbox sopra, l’input aggiorna solo la variabile di state message. Dal punto di vista dell’utente, questo non dovrebbe influire sulla connessione della chat. Tuttavia, ogni volta che aggiorni message, il tuo componente si ri-renderizza. Quando il tuo componente si ri-renderizza, il codice al suo interno viene eseguito di nuovo da zero.

Un nuovo oggetto options viene creato da zero a ogni ri-renderizzazione del componente ChatRoom. React vede che l’oggetto options è un oggetto diverso dall’oggetto options creato durante l’ultima renderizzazione. Ecco perché re-sincronizza il tuo Effetto (che dipende da options), e la chat si riconnette mentre digiti.

Questo problema riguarda solo oggetti e funzioni. In JavaScript, ogni oggetto e funzione appena creati sono considerati distinti da tutti gli altri. Non importa che il contenuto al loro interno possa essere lo stesso!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

Le dipendenze su oggetti e funzioni possono far re-sincronizzare il tuo Effetto più spesso di quanto ti serva.

Ecco perché, quando possibile, dovresti cercare di evitare oggetti e funzioni come dipendenze del tuo Effetto. Prova invece a spostarli fuori dal componente, dentro l’Effetto, o a estrarre valori primitivi da essi.

Spostare oggetti e funzioni statici fuori dal componente

Se l’oggetto non dipende da props e state, puoi spostare quell’oggetto fuori dal tuo componente:

const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};

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

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

In questo modo, dimostri al linter che non è reattivo. Non può cambiare a causa di una ri-renderizzazione, quindi non ha bisogno di essere una dipendenza. Ora ri-renderizzare ChatRoom non farà re-sincronizzare il tuo Effetto.

Funziona anche per le funzioni:

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}

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

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

Poiché createOptions è dichiarata fuori dal tuo componente, non è un valore reattivo. Ecco perché non ha bisogno di essere specificata nelle dipendenze del tuo Effetto, e perché non farà mai re-sincronizzare il tuo Effetto.

Spostare oggetti e funzioni dinamici dentro l’Effetto

Se il tuo oggetto dipende da un valore reattivo che può cambiare a causa di una ri-renderizzazione, come una prop roomId, non puoi tirarlo fuori dal tuo componente. Puoi, tuttavia, spostare la sua creazione dentro il codice del tuo Effetto:

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

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

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

Ora che options è dichiarato dentro il tuo Effetto, non è più una dipendenza del tuo Effetto. Invece, l’unico valore reattivo usato dal tuo Effetto è roomId. Poiché roomId non è un oggetto o una funzione, puoi essere sicuro che non sarà involontariamente diverso. In JavaScript, numeri e stringhe sono confrontati per il loro contenuto:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

Grazie a questa correzione, la chat non si riconnette più se modifichi l’input:

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

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

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

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

  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} />
    </>
  );
}

Tuttavia, si riconnette quando cambi il menu a tendina roomId, come ti aspetteresti.

Funziona anche per le funzioni:

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

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

useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Puoi scrivere le tue funzioni per raggruppare pezzi di logica dentro il tuo Effetto. Finché le dichiari anche dentro il tuo Effetto, non sono valori reattivi, e quindi non hanno bisogno di essere dipendenze del tuo Effetto.

Leggere valori primitivi dagli oggetti

A volte, potresti ricevere un oggetto dalle props:

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

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

Il rischio qui è che il componente genitore creerà l’oggetto durante la renderizzazione:

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

Questo farebbe re-sincronizzare il tuo Effetto ogni volta che il componente genitore si ri-renderizza. Per correggere, leggi le informazioni dall’oggetto fuori dall’Effetto, ed evita di avere dipendenze su oggetti e funzioni:

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

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

La logica diventa un po’ ripetitiva (leggi alcuni valori da un oggetto fuori dall’Effetto, e poi crei un oggetto con gli stessi valori dentro l’Effetto). Ma rende molto esplicito da quali informazioni il tuo Effetto effettivamente dipende. Se un oggetto viene ricreato involontariamente dal componente genitore, la chat non si riconnetterebbe. Tuttavia, se options.roomId o options.serverUrl sono davvero diversi, la chat si riconnetterebbe.

Calcolare valori primitivi dalle funzioni

Lo stesso approccio può funzionare per le funzioni. Per esempio, supponiamo che il componente genitore passi una funzione:

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

Per evitare di renderla una dipendenza (e farla riconnettere alle ri-renderizzazioni), chiamala fuori dall’Effetto. Questo ti dà i valori roomId e serverUrl che non sono oggetti, e che puoi leggere dall’interno del tuo Effetto:

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

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

Questo funziona solo per funzioni puri perché sono sicure da chiamare durante la renderizzazione. Se la tua funzione è un gestore di eventi, ma non vuoi che i suoi cambiamenti re-sincronizzino il tuo Effetto, avvolgila in un Effect Event invece.

Riepilogo

  • Le dipendenze devono sempre corrispondere al codice.
  • Quando non sei soddisfatto delle tue dipendenze, ciò che devi modificare è il codice.
  • Sopprimere il linter porta a bug molto confusi, e dovresti sempre evitarlo.
  • Per rimuovere una dipendenza, devi “dimostrare” al linter che non è necessaria.
  • Se del codice dovrebbe essere eseguito in risposta a una specifica interazione, sposta quel codice in un gestore di eventi.
  • Se parti diverse del tuo Effetto dovrebbero rieseguirsi per motivi diversi, dividilo in più Effetti.
  • Se vuoi aggiornare dello state in base allo state precedente, passa una funzione updater.
  • Se vuoi leggere l’ultimo valore senza “reagire” ad esso, estrai un Effect Event dal tuo Effetto.
  • In JavaScript, oggetti e funzioni sono considerati diversi se sono stati creati in momenti diversi.
  • Cerca di evitare dipendenze su oggetti e funzioni. Spostali fuori dal componente o dentro l’Effetto.

Sfida 1 di 4:
Correggere un intervallo che si resetta

Questo Effetto configura un intervallo che scatta ogni secondo. Hai notato qualcosa di strano: sembra che l’intervallo venga distrutto e ricreato ogni volta che scatta. Correggi il codice in modo che l’intervallo non venga ricreato costantemente.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}