Efektin riippuvuuksien poistaminen

Kun kirjoitat Efektia, linter tarkistaa, että olet sisällyttänyt jokaisen reaktiivisen arvon (kuten propsit ja tilan) Efektisi riippuvuuslistalle. Tämä varmistaa, että Efektisi pysyy synkronoituna komponenttisi viimeisimpien propsien ja tilan kanssa. Tarpeettomat riippuvuudet voivat aiheuttaa Efektisi suorittamisen liian usein tai jopa luoda äärettömän silmukan. Seuraa tätä opasta tarkistaaksesi ja poistaaksesi tarpeettomat riippuvuudet Efekteistäsi.

Tulet oppimaan

  • Miten korjata loputtomat Efekti-riippuvuus-silmukat
  • Mitä tehdä kun haluat poistaa riippuvuuden
  • Miten lukea arvo Efektistasi “reagoimatta” siihe
  • Miten ja miksi välttää olioiden ja funktioiden riippuvuuksia
  • Miksi riippuvuus-linterin hiljentäminen on vaarallista ja mitä tehdä sen sijaan

Riippuvuuksien tulisi vastata koodia

Kun kirjoitat Efektia, sinun täytyy ensin määritellä miten aloittaa ja lopettaa mitä ikinä haluat Efektisi tekevän:

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

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

Sitten, jos jätät Efektisi riippuvuudet tyhjäksi ([]), linter suosittelee oikeita riippuvuuksia:

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

Täytä ne sen mukaan mitä linter kertoo:

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Kaikki riippuvuudet määritelty
// ...
}

Efektit “reagoivat” reaktiivisiin arvoihin. Koska roomId on reaktiivinen arvo (se voi muuttua renderöinnin seurauksena), linter tarkistaa, että olet määritellyt sen riippuvuutena. Jos roomId vastaanottaa eri arvon, React synkronoi Efektisi uudelleen. Tämä takaa, että chat pysyy yhdistettynä valittuun huoneeseen ja “reagoi” pudotusvalikkoon:

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

Poistaaksesi riippuvuuden, todista ettei se ole riippuvuus

Huomaa, ettet voi “päättää” Efektisi riippuvuuksia. Jokainen reaktiivinen arvo, jota Efektisi koodi käyttää, täytyy olla määritelty riippuvuuslistalla. Riippuvuuslista määräytyy ympäröivän koodin mukaan:

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

function ChatRoom({ roomId }) { // Tämä on reaktiivinen arvo
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Tämä Efekti lukee reaktiivisen arvon
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Joten sinun täytyy määritellä tämä reaktiivinen arvo Efektisi riippuvuudeksi
// ...
}

Reaktiiviset arvot ovat propsit ja kaikki muuttujat ja funktiot määritelty suoraan komponentin sisällä. Koska roomId on reaktiivinen arvo, et voi poistaa sitä riippuvuuslistalta. Linter ei sallisi sitä:

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'
// ...
}

Ja linter on oikeassa! Koska roomId voi muuttua ajan myötä, tämä aiheuttaisi bugin koodiisi.

Poistaaksesi riippuvuuden, “todista” linterille, että sen ei tarvitse olla riippuvuus. Voit esimerkiksi siirtää roomId komponentin ulkopuolelle todistaaksesi, ettei se ole reaktiivinen eikä muutu uudelleenrenderöinneissä:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Ei ole reaktiivinen arvo enää

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Kaikki riippuvuudet määritelty
// ...
}

Now that roomId is not a reactive value (and can’t change on a re-render), it doesn’t need to be a dependency:

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

Tämän takia voit nyt määritellä tyhjän ([]) riippuvuuslistan. Efektisi ei todellakaan riipu enää yhdestäkään reaktiivisesta arvosta, joten sitä ei todellakaan tarvitse suorittaa uudelleen kun komponentin propsit tai tila muuttuvat.

Muuttaaksesi riippuvuuksia, muuta koodia

Olet saattanut huomata kaavan työskentelyssäsi:

  1. Ensiksi, muutat Efektisi koodia tai miten reaktiiviset arvot on määritelty.
  2. Sitten, seuraa linteria ja muuta riippuvuudet vastaamaan muuttunutta koodia.
  3. Jos et ole tyytyväinen riippuvuuslistaan, voit palata ensimmäiseen vaiheeseen (ja muuttaa koodia uudelleen).

Viimeinen kohta on tärkeä. Jos haluat muuttaa riippuvuuksia, muuta ensin ympäröivää koodia. Voit ajatella riippuvuuslistaa listana kaikista Efektisi koodissa käytetyistä reaktiivisista arvoista. Et valitse mitä listaan laitat. Lista kuvastaa koodiasi. Muuttaaksesi riippuvuuslistaa, muuta koodia.

Tämä saattaa tuntua yhtälön ratkaisemiselta. Saatat aloittaa tavoitteesta (esimerkiksi poistaa riippuvuus), ja sinun täytyy “löytää” koodi, joka vastaa tavoitetta. Kaikki eivät pidä yhtälöiden ratkaisemisesta, ja samaa voisi sanoa Efektien kirjoittamisesta! Onneksi alla on lista yleisistä resepteistä, joita voit kokeilla.

Sudenkuoppa

Jos sinulla on olemassa oleva koodipohja, sinulla saattaa olla joitain Efektejä, jotka hiljentävät linterin näin:

useEffect(() => {
// ...
// 🔴 Vältä linterin hiljentämistä tällä tavalla:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Kun riippuvuudet eivät vastaa koodia, on erittäin suuri riski, että aiheutat bugeja. Hiljentämällä linterin, “valehtelet” Reactille Efektisi riippuvuuksista.

Sen sijaan, käytä alla olevia tekniikoita.

Syväsukellus

Miksi linterin hiljentäminen on niin vaarallista?

Linterin hiljentäminen johtaa erittäin epäintuitiivisiin bugeihin, jotka ovat vaikeita löytää ja korjata. Tässä on yksi esimerkki:

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

Sanotaan, että haluat suorittaa Efektin “vain mountissa”. Olet lukenut, että tyhjät ([]) riippuvuudet tekevät sen, joten olet päättänyt hiljentää linterin ja määritellä [] riippuvuudeksi.

Tämän laskurin oli tarkoitus kasvaa joka sekunti kahdella painikkeella määriteltävällä määrällä. Kuitenkin, koska “valehtelit” Reactille, että tämä Efekti ei riipu mistään, React käyttää ikuisesti onTick funktiota ensimmäisestä renderöinnistä. Tuon renderöinnin aikana, count oli 0 ja increment oli 1. Tämän takia onTick tuosta renderöinnistä kutsuu aina setCount(0 + 1) joka sekunti, ja näet aina 1. Tällaisia bugeja on vaikeampi korjata kun ne ovat levinneet useisiin komponentteihin.

On aina parempi vaihtoehto kuin linterin hiljentäminen! Korjataksesi tämän koodin, lisää onTick riippuvuuslistalle. (Varmistaaksesi, että laskuri asetetaan vain kerran, tee onTick:sta Efektitapahtuma.)

Suosittelemme kohtelemaan riippuvuus-linterin virhettä käännösvirheenä. Jos et hiljennä sitä, et koskaan näe tällaisia bugeja. Loput tästä sivusta listaavat vaihtoehtoja tähän ja muihin tapauksiin.

Turhien riippuvuuksien poistaminen

Joka kerta kun muutat Efektisi riippuvuuksia koodin mukaan, katso riippuvuuslistaa. Onko järkevää, että Efekti suoritetaan uudelleen kun jokin näistä riippuvuuksista muuttuu? Joskus vastaus on “ei”:

  • Saatat haluta suorittaa eri osia Efektistä eri olosuhteissa.
  • Saatat haluta lukea viimeisimmän arvon jostain riippuvuudesta sen sijaan, että “reagoisit” sen muutoksiin.
  • Riippuvuus voi muuttua liian usein vahingossa koska se on olio tai funktio.

Löytääksesi oikean ratkaisun, sinun täytyy vastata muutamaan kysymykseen Efektistäsi. Käydään ne läpi.

Pitäisikö tämä koodi siirtää tapahtumankäsittelijään?

Ensimmäinen asia, jota sinun tulisi ajatella on pitäisikö tämä koodi olla Efekti ollenkaan.

Kuvittele lomake. Kun lähetät sen, asetat submitted tilamuuttujan arvoksi true. Sinun täytyy lähettää POST-pyyntö ja näyttää ilmoitus. Olet laittanut tämän logiikan Efektiin, joka “reagoi” submitted arvon ollessa true:

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

useEffect(() => {
if (submitted) {
// 🔴 Vältä: Tapahtumakohtainen logiikka Efektissa
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Myöhemmin, haluat tyylittää ilmoituksen viestin nykyisen teeman mukaan, joten luet nykyisen teeman. Koska theme on määritelty komponentin sisällä, se on reaktiivinen arvo, joten lisäät sen riippuvuudeksi:

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

useEffect(() => {
if (submitted) {
// 🔴 Vältä: Tapahtumakohtainen logiikka Efektissa
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ Kaikki riippuvuudet määritelty

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Tekemällä tämän, olet aiheuttanut bugin. Kuvittele, että lähetät lomakkeen ensin ja sitten vaihdat teeman tummaksi. theme muuttuu, Efekti suoritetaan uudelleen, ja se näyttää saman ilmoituksen uudelleen!

Ongelma on, että sen ei pitäisi olla Efekti alunperinkään. Haluat lähettää POST-pyynnön ja näyttää ilmoituksen vastauksena lomakkeen lähettämisen, joka on tietty interaktio. Suorittaaksesi koodia tiettyyn interaktioon, laita se suoraan vastaavaan tapahtumankäsittelijään:

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

function handleSubmit() {
// ✅ Hyvä: Tapahtumakohtainen logiikka kutsutaan tapahtumakäsittelijästä
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

Nyt kun koodi on tapahtumakäsittelijässä, se ei ole reaktiivista—joten se suoritetaan vain kun käyttäjä lähettää lomakkeen. Lue valinta tapahtumankäsittelijän ja Efektin välillä ja miten poistaa turhia Efekteja.

Tekeekö Efektisi useita toistaan riippumattomia asioita?

Seuraava kysymys jota sinun tulisi kysyä itseltäsi on tekeekö Efektisi useita toistaan riippumattomia asioita.

Kuvittele että luot toimituslomakkeen, jossa käyttäjän täytyy valita kaupunki ja alue. Haet cities listan palvelimelta valitun country:n mukaan näyttääksesi ne pudotusvalikossa:

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]); // ✅ Kaikki riippuvuudet määritelty

// ...

Tämä on esimerkki datan hakemisesta Efektissä. Synkronoit cities tilan verkon kanssa country propsin mukaan. Et voi tehdä tätä tapahtumankäsittelijässä koska sinun täytyy hakea heti kun ShippingForm näytetään ja aina kun country muuttuu (riippumatta siitä mikä interaktio aiheuttaa sen).

Sanotaan, että lisäät toisen pudotusvalikon kaupunkien alueille, joka hakee areas valitun city:n mukaan. Saatat aloittaa lisäämällä toisen fetch kutsun alueiden listalle samassa Efektissä:

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);
}
});
// 🔴 Vältä: Yksi Efekti synkronoi kahta erillistä prosessia
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ Kaikki riippuvuudet määritelty

// ...

Kuitenkin, koska Efekti käyttää nyt city tilamuuttujaa, olet joutunut lisäämään city riippuvuuslistalle. Tämä aiheutti ongelman: kun käyttäjä valitsee eri kaupungin, Efekti suoritetaan uudelleen ja kutsuu fetchCities(country). Tämän takia haet tarpeettomasti kaupunkilistan monta kertaa.

Ongelma tässä koodissa on, että synkronoit kaksi erillistä asiaa:

  1. Haluat synkronoida cities tilan verkon kanssa country propsin mukaan.
  2. Haluat synkronoida areas tilan verkon kanssa city tilan mukaan.

Jaa logiikka kahteen Efektiin, joista kumpikin reagoi siihen propiin, jonka kanssa se tarvitsee synkronoida:

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]); // ✅ Kaikki riippuvuudet määritelty

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]); // ✅ Kaikki riippuvuudet määritelty

// ...

Nyt ensimmäinen Efekti suoritetaan vain jos country muuttuu, kun taas toinen Efekti suoritetaan kun city muuttuu. Olet erottanut ne tarkoituksen mukaan: kaksi erillistä asiaa synkronoidaan kahdella erillisellä Efektillä. Kahdella erillisellä Efektillä on kaksi erillistä riippuvuuslistaa, joten ne eivät käynnistä toisiaan vahingossa.

Lopullinen koodi on pidempi kuin alkuperäinen, mutta näiden Efektien jakaminen on silti oikein. Kunkin Efektin tulisi edustaa erillistä synkronointiprosessia. Tässä esimerkissä, yhden Efektin poistaminen ei riko toisen Efektin logiikkaa. Tämä tarkoittaa, että ne synkronoivat eri asioita, ja on hyvä jakaa ne osiin. Jos olet huolissasi toistosta, voit parantaa tätä koodia poistamalla toistuvan logiikan omaksi Hookiksi.

Luetko jotain tilaa laskeaksesi seuraavan tilan?

Tämä Efekit päivittää messages tilamuuttujan uudella taulukolla joka kerta kun uusi viesti saapuu:

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

Se käyttää messages muuttujaa luodakseen uuden taulukon joka alkaa kaikilla olemassaolevilla viesteillä ja lisää uuden viestin loppuun. Koska messages on reaktiivinen arvo, jota Efekti lukee, sen täytyy olla riippuvuus:

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]); // ✅ Kaikki muuttujat määritelty
// ...

Ja messages muuttujan lisääminen riippuvuudeksi aiheuttaa ongelman.

Joka kerta kun vastaanotat viestin, setMessages() aiheuttaa komponentin uudelleen renderöinnin uudella messages taulukolla, joka sisältää vastaanotetun viestin. Kuitenkin, koska tämä Efekti riippuu nyt messages muuttujasta, tämä synkronoi myös Efektin uudelleen. Joten jokainen uusi viesti aiheuttaa chatin uudelleenyhdistämisen. Käyttäjä ei pitäisi siitä!

Korjataksesi ongelman, älä lue messages tilaa Efektissä. Sen sijaan, välitä päivittäjäfunktion setMessages:lle:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Huomaa miten Efektisi ei lue messages muuttujaa ollenkaan. Sinun täytyy vain välittää päivittäjäfunktio kuten msgs => [...msgs, receivedMessage]. React laittaa päivittäjäfunktion jonoon ja tarjoaa msgs argumentin sille seuraavassa renderöinnissä. Tämän takia Efektin ei tarvitse enää riippua messages muuttujasta. Tämän korjauksen takia, chatin viestin vastaanottaminen ei enää aiheuta chatin uudelleenyhdistämistä.

Haluatko lukea arvon “reagoimatta” sen muutoksiin?

Kehitteillä

Tämä osio kuvailee kokeellista API:a, joka ei ole vielä julkaistu vakaassa Reactin versiossa.

Oletetaan, että haluat toistaa äänen kun käyttäjä vastaanottaa uuden viestin, ellei isMuted ole 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();
}
});
// ...

Koska Efektisi käyttää nyt isMuted koodissaan, sinun täytyy lisätä se riippuvuuslistalle:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Ongelma on joka kerta kun isMuted muuttuu (esimerkiksi, kun käyttäjä painaa “Muted” kytkintä), Efekti synkronoituu uudelleen ja yhdistää uudelleen chattiin. Tämä ei ole haluttu käyttäjäkokemus! (Tässä esimerkissä, jopa linterin poistaminen ei toimisi—jos teet sen, isMuted jäisi “jumiin” vanhaan arvoonsa.)

Ratkaistaksesi tämän ongelman, sinun täytyy erottaa logiikka, joka ei saisi olla reaktiivista Efektistä. Et halua tämän Efektin “reagoivan” isMuted muutoksiin. Siirrä tämä ei-reaktiivinen logiikka Efektitapahtumaan:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Efektitapahtumien avulla voit jakaa Efektit reaktiivisiin osiin (joiden tulisi “reagoida” reaktiivisiin arvoihin kuten roomId ja niiden muutoksiin) ja ei-reaktiivisiin osiin (jotka lukevat vain viimeisimmät arvot, kuten onMessage lukee isMuted). Nyt kun luet isMuted tilan Efektitapahtumassa, sen ei tarvitse olla Efektisi riippuvuus. Tämän takia chat ei yhdistä uudelleen kun kytket “Muted” asetuksen päälle ja pois, ratkaisten alkuperäisen ongelman!

Tapahtumankäsittelijän kääriminen propseista

Saatat törmätä samanlaiseen ongelmaan kun komponenttisi vastaanottaa tapahtumankäsittelijän propsina:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Oletetaan, että vanhempi komponentti lähettää eri onReceiveMessage funktion joka renderöinnillä:

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

Koska onReceiveMessage on riippuvuus, sen tulisi aiheuttaa Efektin uudelleensynkronointi jokaisen yläkomponentin renderöinnin yhteydessä. Tämä saisi sen yhdistämään uudelleen chattiin. Ratkaistaksesi tämän, kääri kutsu Efektitapahtumaan:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Efektitapahtumat eivät ole reaktiivisia, joten sinun ei tarvitse määritellä niitä riippuvuuksiksi. Tämän takia chat ei yhdistä uudelleen vaikka yläkomponentti lähettäisi funktion joka on eri jokaisella renderöinnillä.

Reaktiivisen ja ei-reaktiivisen koodin erottaminen

Tässä esimerkissä, haluat kirjata vierailun joka kerta kun roomId muuttuu. Haluat sisällyttää nykyisen notificationCount jokaiseen lokiin, mutta et halua muutoksen notificationCount tilaan käynnistävän lokitapahtumaa.

Ratkaisu on jälleen jakaa ei-reaktiivinen koodi Efektitapahtumaan:

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

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ Kaikki riippuvuudet määritelty
// ...
}

Haluat logiikkasi olevan reaktiivista roomId suhteen, joten luet roomId Efektissä. Kuitenkin, et halua muutoksen notificationCount tilaan kirjaavan ylimääräistä vierailua, joten luet notificationCount Efektitapahtumassa. Lue lisää viimeisimpien propsien ja tilan lukemisesta Efekteistä käyttäen Efektitapahtumia.

Muuttuuko jokin reaktiivinen arvo tarkoituksettomasti?

Joskus haluat Efektisi “reagoivan” tiettyyn arvoon, mutta arvo muuttuu useammin kuin haluaisit—ja se ei välttämättä heijasta mitään todellista muutosta käyttäjän näkökulmasta. Esimerkiksi, sanotaan että luot options olion komponentin sisällä, ja luet sitten sen olion Efektistä:

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

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

Tämä olio on määritelty komponentin sisällä, joten se on reaktiivinen arvo Kun luet tämän kaltaisen reaktiivisen arvon Efektin sisällä, määrittelet sen riippuvuudeksi. Tämä varmistaa, että Efektisi “reagoi” sen muutoksiin:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Kaikki riippuvuudet määritelty
// ...

On tärkeää määritellä se riippuvuudeksi! Tämä takaa, jos esimerkiksi roomId muuttuisi, Efektisi yhdistäisi uudelleen chattiin uusilla options arvoilla. Kuitenkin, ylläolevassa koodissa on myös ongelma. Nähdäksesi sen, kokeile kirjoittaa syöttölaatikkoon alla olevassa hiekkalaatikossa, ja katso mitä tapahtuu konsolissa:

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

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

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

  // Tilapäisesti poista linter käytöstä ongelman osoittamiseksi
  // 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} />
    </>
  );
}

Yllä olevassa esimerkissä, syöttölaatikko päivittää vain message tilamuuttujaa. Käyttäjän näkökulmasta, tämä ei vaikuta chat-yhteyteen. Kuitenkin, joka kerta kun päivität message tilaa, komponenttisi renderöityy. Kun komponenttisi renderöityy, koodi sen sisällä ajetaan alusta asti.

Uusi options olio luodaan alusta asti jokaisella ChatRoom komponentin uudelleenrenderöinnillä. React näkee, että options olio on eri olio kuin options olio, joka luotiin edellisellä renderöinnillä. Tämän takia se synkronoi uudelleen Efektisi (joka riippuu options arvosta), ja chat yhdistää uudelleen kun kirjoitat.

Tämä ongelma vaikuttaa vain olioihin ja funktioihin. JavaScriptissä, jokainen uusi luotu olio ja funktio katsotaan erilaiseksi kuin kaikki muut. Ei ole väliä, että niiden sisältö voi olla sama!

// Ensimmäisen renderöinnin aikana
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// Seuraavan renderöinnin aikana
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// Nämä ovat kaksi eri oliota!
console.log(Object.is(options1, options2)); // false

Olion ja funktion riippuvuudet voivat saada Efektisi synkronoimaan useammin kuin tarvitset.

Tämän takia aina kun mahdollista, sinun tulisi pyrkiä välttämään olioita ja funktiota Efektin riippuvuuksina. Sen sijaan, kokeile siirtää niitä ulos komponentista, sisälle Efektiin, tai saada primitiiviset arvot niistä.

Siirrä staattiset oliot ja funktiot komponentin ulkopuolelle

Jos olio ei riipu mistään propseista tai tilasta, voit siirtää sen ulos komponentistasi:

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

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

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Kaikki riippuvuudet määritelty
// ...

Tällä tavalla todistat linterille, ettei se ole reaktiivinen. Se ei voi muuttua uudelleenrenderöinnin seurauksena, joten sen ei tarvitse olla riippuvuus. Nyt ChatRoom uudelleenrenderöinti ei aiheuta Efektisi uudelleensynkronointia.

Tämä toimii myös funktioille:

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

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

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

Koska createOptions on määritelty komponentin ulkopuolella, se ei ole reaktiivinen arvo. Tämän takia sen ei tarvitse olla määritelty Efektisi riippuvuuksissa, eikä se koskaan aiheuta Efektisi uudelleensynkronointia.

Siirrä dynaamiset oliot ja funktiot Efektin sisään

Jos oliosi riippuu jostain reaktiivisesta arvosta, joka voi muuttua renderöinnin yhteydessä, kuten roomId propsi, et voi siirtää sitä komponentin ulkopuolelle. Voit kuitenkin siirtää sen luomisen koodin Efektisi sisälle:

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
// ...

Nyt kun options on määritelty Efektisi sisällä, se ei ole enää Efektisi riippuvuus. Sen sijaan, ainoa Efektisi käyttämä reaktiivinen arvo on roomId. Koska roomId ei ole oli taikka funktio, voit olla varma ettei se ole tahattomasti eri. JavaScriptissa, numerot ja merkkijonot verrataan niiden sisällön perusteella:

// Ensimmäisen renderöinnin aikana
const roomId1 = 'music';

// Seuraavan renderöinnin aikana
const roomId2 = 'music';

// Nämä kaksi merkkijonoa vastaavat toisiaan!
console.log(Object.is(roomId1, roomId2)); // true

Kiitos tämän korjauksen, chat ei enää yhdistä uudelleen jos muutat syöttölaatikkoa:

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

Kuitenkin, se yhdistää uudelleen kun vaihdat roomId pudotusvalikkosta, kuten odotit.

Tämä toimii myös funktioille:

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
// ...

Voit kirjoittaa omia funktioita ryhmitelläksesi logiikkaa Efektisi sisällä. Niin kauan kuin määrittelet ne Efektisi sisällä, ne eivät ole reaktiivisia arvoja, ja näin ollen ne eivät tarvitse olla Efektisi riippuvuuksia.

Lue primitiiviset arvot oliosta

Joskus saatat saada objektin propseista:

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

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Kaikki riippuvuudet määritelty
// ...

Riskinä tässä on että vanhempi komponentti luo olion renderöinnin aikana:

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

Tämä aiheuttaisi Efektisi yhdistävän uudelleen joka kerta kun vanhempi komponentti renderöi uudelleen. Korjataksesi tämän, lue informaatio oliosta Efektin ulkopuolella, ja vältä olion ja funktion riippuvuuksia:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Logiikasta tulee toistuvaa (luet arvoja objektista Efektin ulkopuolella, ja sitten luot objektin samoilla arvoilla Efektin sisällä). Mutta se tekee hyvin eksplisiittiseksi minkä informaation Efektisi oikeasti riippuu. Jos objekti luodaan uudelleen tahattomasti vanhemman komponentin toimesta, chat ei yhdistä uudelleen. Kuitenkin, jos options.roomId tai options.serverUrl ovat todella erilaisia, chat yhdistää uudelleen.

Laske primitiiviset arvot funktioissa

Sama tapa voi toimia myös funktioille. Esimerkiksi, oletetaan että vanhempi komponentti välittää funktion:

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

Välttääksesi tekemästä siitä riippuvuuden (ja aiheuttamasta uudelleen yhdistämistä renderöinneissä), kutsu sitä Efektin ulkopuolella. Tämä antaa sinulle roomId ja serverUrl arvot, jotka eivät ole objekteja, ja joita voit lukea Efektisi sisältä:

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]); // ✅ Kaikki riippuvuudet määritelty
// ...

Tämä toimii ainoastaan puhtaille funktioille, koska ne ovat turvallisia kutsua renderöinnin aikana. Jos fuktiosi on tapahtumankäsittelijä, mutta et halua sen muutosten synkronoivan Efektisi, kääri se Efektitapahtumaan.

Kertaus

  • Riippuvuuksien tulisi aina vastata koodia.
  • Kun et ole tyytyväinen riippuvuuksiisi, mitä sinun tulee muokata on koodi.
  • Linterin hiljentäminen johtaa hyvin hämmentäviin bugeihin, ja sinun tulisi aina välttää sitä.
  • Poistaaksesi riippuvuuden, sinun täytyy “todistaa” linterille, että se ei ole tarpeellinen.
  • Jos jokin koodi tulisi suorittaa vastauksena tiettyyn vuorovaikutukseen, siirrä koodi tapahtumankäsittelijään.
  • Jos Efektisi eri osat tulisi suorittaa eri syistä, jaa se useaksi Efektiksi.
  • Jos haluat päivittää jotain tilaa aikaisemman tilan perusteella, välitä päivitysfunktio.
  • Jos haluat lukea viimeisimmän arvon “reagoimatta” siihen, luo Efektitapahtuma Efektistäsi.
  • JavaScriptissä oliot ja funktiot ovat erilaisia jos ne on luotu eri aikoina.
  • Pyri välttämään oliota ja funktioita riippuvuuksina. Siirrä ne komponentin ulkopuolelle tai Efektin sisälle.

Haaste 1 / 4:
Korjaa nollautuva laskuri

Tämä Efekti asettaa laskurin joka laskee joka sekunti. Huomaat jotain outoa tapahtuvan: näyttää siltä että laskuri tuhotaan ja luodaan uudelleen joka kerta kun se laskee. Korjaa koodi niin että laskuri ei tuhoudu jatkuvasti.

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>
}