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:
- Ensiksi, muutat Efektisi koodia tai miten reaktiiviset arvot on määritelty.
- Sitten, seuraa linteria ja muuta riippuvuudet vastaamaan muuttunutta koodia.
- 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.
Syväsukellus
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 Tapahtumankäsittelijästä
post('/api/register');
showNotification('Successfully registered!', theme);
}
// ...
}
Nyt kun koodi on Tapahtumankä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:
- Haluat synkronoida
cities
tilan verkon kanssacountry
propsin mukaan. - Haluat synkronoida
areas
tilan verkon kanssacity
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?
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(options);
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> }