Et ehkä tarvitse Effectia

Efektit ovat pelastusluukku React-paradigmasta. Niiden avulla voit “astua ulos” Reactista ja synkronoida komponenttejasi jonkin ulkoisen järjestelmän, kuten ei-React-widgetin, verkon tai selaimen DOM:in kanssa. Jos ulkoista järjestelmää ei ole mukana (esimerkiksi jos haluat päivittää komponentin tilan, kun joitain propseja tai tiloja muutetaan), sinun ei pitäisi tarvita Effektia. Tarpeettomien efektien poistaminen tekee koodistasi helpommin seurattavan, nopeamman suorittaa ja vähemmän virhealttiin.

Tulet oppimaan

  • Miksi ja miten poistaa tarpeettomat Effektit komponenteistasi
  • Miten välimuistittaa kalliit laskutoimitukset ilman Effekteja
  • Miten nollata ja säätää komponentin tilaa ilman Effekteja
  • Miten jakaa logiikkaa tapahtumankäsittelijöiden välillä
  • Millainen logiikka tulisi siirtää tapahtumankäsittelijöihin
  • Miten ilmoittaa muutoksista vanhemmille komponenteille

Miten poistaa turhia Effecteja

On kaksi yleistä tapausta, joissa et tarvitse efektejä:

  • Et tarvitse efektejä datan muokkaamiseen renderöintiä varten. Esimerkiksi, sanotaan että haluat suodattaa listaa ennen sen näyttämistä. Saatat tuntea houkutuksen efektin kirjoittamiseen, joka päivittää tilamuuttujan, kun lista muuttuu. Kuitenkin tämä on tehottomaa. Kun päivität tilaa, React ensin kutsuu komponenttifunktioitasi laskemaan, mitä tulisi näytölle. Sitten React “kommittaa” nämä muutokset DOMiin päivittäen näytön. Sitten React suorittaa efektit. Jos efektisi myös päivittää välittömästi tilaa, tämä käynnistää koko prosessin alusta! Välttääksesi tarpeettomat renderöintikierrokset, muokkaa kaikki data komponenttiesi ylätasolla. Tuo koodi ajetaan automaattisesti aina kun propsit tai tila muuttuvat.
  • Et tarvitse efektejä käsittelemään käyttäjätapahtumia. Esimerkiksi, oletetaan että haluat lähettää /api/buy POST-pyynnön ja näyttää ilmoituksen, kun käyttäjä ostaa tuotteen. Osta-nappulan klikkaustapahtumankäsittelijässä tiedät tarkalleen mitä tapahtui. Kun efekti suoritetaan, et tiedä mitä käyttäjä teki (esimerkiksi, minkä nappulan hän klikkasi). Tämän vuoksi käyttäjätapahtumat käsitellään yleensä vastaavissa tapahtumankäsittelijöissä.

Tarvitset kyllä efektejä synkronoimiseen ulkoisten järjestelmien kanssa. Esimerkiksi voit kirjoittaa efektin, joka pitää jQuery-widgetin synkronoituna Reactin tilan kanssa. Voit myös noutaa tietoja efekteillä: esimerkiksi voit pitää hakutulokset synkronoituna nykyisen hakukyselyn kanssa. On kuitenkin hyvä pitää mielessä, että nykyaikaiset kehysratkaisut tarjoavat tehokkaampia sisäänrakennettuja tiedonhakumekanismeja kuin efektien kirjoittaminen suoraan komponentteihin.

Katsotaanpa joitakin yleisiä konkreettisia esimerkkejä saadaksesi oikeanlaisen intuition.

Tilan päivittäminen propsin tai tilan pohjalta

Oletetaan, että sinulla on komponentti, jossa on kaksi tilamuuttujaa: firstName ja lastName. Haluat laskea niistä fullName-nimen yhdistämällä ne. Lisäksi haluat, että fullName päivittyy aina, kun firstName tai lastName muuttuvat. Ensimmäinen vaistosi saattaa olla lisätä fullName-tilamuuttuja ja päivittää se effektissa:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Vältä: turha tila ja tarpeeton Effekti
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}

Tämä on tarpeettoman monimutkainen. Se on myös tehotonta: se suorittaa koko renderöinnin vanhentuneella fullName-arvolla ja päivittää sen sitten välittömästi uudelleen päivitetyllä arvolla. Poista tilamuuttuja ja Effekti:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Hyvä: lasketaan renderöinnin aikana
const fullName = firstName + ' ' + lastName;
// ...
}

Kun jotain voidaan laskea olemassa olevista propseista tai tilamuuttujista, älä aseta sitä tilaan. Sen sijaan laske se renderöinnin aikana. Tämä tekee koodistasi nopeamman (vältät ylimääräiset “kaskadiset” päivitykset), yksinkertaisemman (poistat osan koodista) ja vähemmän virhealttiin (vältät bugeja, jotka johtuvat tilamuuttujien epäsynkronoinnista). Jos tämä lähestymistapa tuntuu uudelta sinulle, Ajattelu Reactissa selittää, mitä tilaan tulisi laittaa.

Raskaiden laskujen välimuistittaminen

Tämä komponentti laskee visibleTodos-muuttujan ottamalla todos-muuttujan propsina vastaan ja suodattamalla sen filter-propsin perusteella. Saatat tuntea houkutuksen tallentaa tulos tilaan ja päivittää sen Effektin avulla:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');

// 🔴 Vältä: turha tila ja tarpeeton Effekti
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

// ...
}

Kuten aiemmassa esimerkissä, tämä on sekä tarpeeton että tehoton. Poista ensin tila ja Effekti:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Tämä on okei jos getFilteredTodos() ei ole hidas.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}

Useiten, tämä koodi on okei! Mutta ehkä getFilteredTodos() on hidas tai sinulla on useita todos kohteita. Tässä tapauksessa et halua laskea getFilteredTodos() uudelleen, jos jokin epäolennainen tilamuuttuja, kuten newTodo, on muuttunut.

Voit välimuistittaa (tai “memoisoida”) kalliin laskutoimituksen käärimällä sen useMemo-Hookin sisään:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Ei suoriteta uudelleen, elleivät todos tai filter muutu
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}

Tai kirjoitettuna yhtenä rivinä:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos()-funktiota ei suoriteta uudelleen, elleivät todos tai filter muutu.
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}

Tämä kertoo Reactille, että et halua sisäisen funktion suorittuvan uudelleen, elleivät todos tai filter ole muuttuneet. React muistaa getFilteredTodos()-funktion palautusarvon ensimmäisellä renderöinnillä. Seuraavilla renderöinneillä se tarkistaa, ovatko todos tai filter erilaisia. Jos ne ovat samat kuin viime kerralla, useMemo palauttaa viimeksi tallennetun tuloksen. Mutta jos ne ovat erilaisia, React kutsuu sisäistä funktiota uudelleen (ja tallentaa sen tuloksen).

Funktio, jonka käärit useMemo-Hookin sisään, suoritetaan renderöinnin aikana, joten tämä toimii vain puhtaiden laskutoimitusten kanssa.

Syväsukellus

Kuinka tunnistan, onko laskenta kallis?

Yleisesti ottaen, ellet luo tai silmukoi tuhansia objekteja, se ei todennäköisesti ole kallista. Jos haluat olla varmempi, voit lisätä konsolilokin mittaamaan aikaa, joka kuluu koodin palan suorittamiseen:

console.time('filter taulukko');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter taulukko');

Suorita vuorovaikutus, jota mitataan (esimerkiksi kirjoittaminen syötekenttään). Näet sitten lokit, kuten filter taulukko: 0.15ms konsolissasi. Jos kokonaisaika on merkittävä (esimerkiksi 1ms tai enemmän), saattaa olla järkevää välimuistittaa laskutoimitus. Kokeilun vuoksi voit sitten kääriä laskutoimituksen useMemo-Hookin sisään ja tarkistaa, onko kokonaisaika vähentynyt vai ei:

console.time('filter taulukko');
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter); // Ohita, jos todos ja filter eivät ole muuttuneet.
}, [todos, filter]);
console.timeEnd('filter taulukko');

useMemo ei tee ensimmäistä renderöintiä nopeammaksi. Se auttaa ainoastaan välttämään tarpeetonta työtä päivityksissä.

Pidä mielessä, että koneesi on todennäköisesti nopeampi kuin käyttäjäsi, joten on hyvä idea testata suorituskykyä keinotekoisella hidastuksella. Esimerkiksi Chrome tarjoaa CPU Throttling-vaihtoehdon tätä varten.

Huomaa myös, että suorituskyvyn mittaaminen kehitysvaiheessa ei anna sinulle tarkimpia tuloksia. (Esimerkiksi, kun Strict Mode on päällä, näet jokaisen komponentin renderöityvän kahdesti kerran sijaan.) Saadaksesi tarkimmat ajat, rakenna sovelluksesi tuotantoon ja testaa sitä laitteella, joka käyttäjilläsi on.

Kaiken tilan palauttaminen kun propsi muuttuu

ProfilePage komponentti saa userId propsin. Sivulla on kommenttikenttä, ja käytät comment-tilamuuttujaa sen arvon säilyttämiseen. Eräänä päivänä huomaat ongelman: kun navigoit yhdestä profiilista toiseen, comment-tila ei nollaudu. Tämän seurauksena on helppo vahingossa lähettää kommentti väärälle käyttäjän profiilille. Korjataksesi ongelman, haluat tyhjentää comment-tilamuuttujan aina, kun userId muuttuu:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

// 🔴 Vältä: Tilan resetointi prospin muuttuesssa Effektissa
useEffect(() => {
setComment('');
}, [userId]);
// ...
}

Tämä on tehotonta, koska ProfilePage ja sen lapset renderöityvät ensin vanhentuneella arvolla ja sitten uudelleen. Se on myös monimutkaista, koska sinun täytyisi tehdä tämä jokaisessa komponentissa, jossa on tilaa ProfilePage:n sisällä. Esimerkiksi, jos kommenttikäyttöliittymä on sisäkkäinen, haluat nollata myös sisäkkäisen kommentin tilan.

Sen sijaan, voit kertoa Reactille, että jokainen käyttäjän profiili on käsitteellisesti erilainen profiili antamalla sille eksplisiittisen avaimen. Jaa komponenttisi kahteen ja välitä key-attribuutti ulkoisesta komponentista sisäiseen:

export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}

function Profile({ userId }) {
// ✅ Tämä ja muut alla olevat tilat nollautuvat key:n muuttuessa automaattisesti
const [comment, setComment] = useState('');
// ...
}

Normaalisti, React säilyttää tilan kun sama komponentti on renderöity samaan paikkaan. Antamalla userId:n key-attribuuttina Profile-komponentille, pyydät Reactia kohtelemaan kahta Profile-komponenttia, joilla on eri userId, kahtena eri komponenttina, jotka eivät jaa tilaa. Aina kun avain (jonka olet asettanut userId:ksi) muuttuu, React luo uudelleen DOMin ja nollaa tilan Profile-komponentissa ja kaikissa sen lapsikomponenteissa. Nyt comment-kenttä tyhjenee automaattisesti navigoidessasi profiilien välillä.

Huomaa, että tässä esimerkissä vain ulkoinen ProfilePage-komponentti on exportattu ja näkyvissä muissa projektin tiedostoissa. Komponentit, jotka renderöivät ProfilePage:a, eivät tarvitse välittää avainta sille: ne välittävät userId:n tavallisena propina. Se, että ProfilePage välittää sen key-attribuuttina sisäiselle Profile-komponentille, on toteutuksen yksityiskohta.

Tilan säätäminen kun propsi muuttuu

Joskus saatat haluat nollata tai säätää osan tilasta propin muuttuessa, mutta et kaikkea.

List komponetti vastaanottaa listan items propsina ja ylläpitää valittua kohdetta selection-tilamuuttujassa. Haluat nollata selection-tilan null:ksi aina kun items-propiin tulee eri taulukko:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 Vältä: Tilan säätämistä propsin muutoksen pohjalta Effektissa
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

Tämä myöskään ei ole ideaali. Joka kerta kun items muuttuu, List ja sen lapsikomponentit renderöityvät ensin vanhentuneella selection-arvolla. Sitten React päivittää DOMin ja suorittaa Efektit. Lopuksi, setSelection(null)-kutsu aiheuttaa uuden renderöinnin List-komponentille ja sen lapsikomponenteille, käynnistäen tämän koko prosessin uudelleen.

Aloita poistamalla Effekti. Sen sijaan, säädä tila suoraan renderöinnin aikana:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// Parempi: Säädä tila renderöinnin aikana
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

Tiedon tallentaminen edellisistä renderöinneistä kuten tässä voi olla hankalaa ymmärtää, mutta se on parempi kuin saman tilan päivittäminen Effektissa. Yllä olevassa esimerkissä setSelection kutsutaan suoraan renderöinnin aikana. React renderöi List-komponentin välittömästi sen jälkeen, kun se poistuu return-lauseella. React ei ole vielä renderöinyt List-lapsia tai päivittänyt DOMia, joten tämän avulla List-lapset voivat ohittaa vanhentuneen selection-arvon renderöinnin.

Kun päivität komponenttia kesken renderöinnin, React heittää pois palautetun JSX:n ja välittömästi yrittää renderöintiä uudelleen. Välttääksesi hyvin hitaat kaskadiset uudelleenyritykset, React sallii saman komponentin tilapäivityksen renderöinnin aikana. Jos päivität toisen komponentin tilaa renderöinnin aikana, näet virheen. Ehto kuten items !== prevItems on tarpeen välttääksesi silmukoita. Voit säätää tilaa tällä tavalla, mutta kaikki muut sivuvaikutukset (kuten DOMin muuttaminen tai timeoutin asettaminen) tulisi pysyä tapahtumankäsittelijöissä tai Efekteissä pitääksesi komponentit puhtaina.

Vaikka tämä malli on tehokkaampi kuin Effect, useimpien komponenttien ei pitäisi tarvita sitäkään. Riippumatta siitä, miten teet sen, tilan säätäminen propsien tai muiden tilojen pohjalta tekee datavirrasta vaikeampaa ymmärtää ja debugata. Tarkista aina, voitko nollata kaiken tilan avaimella tai laskea kaiken renderöinnin aikana sen sijaan. Esimerkiksi, sen sijaan, että tallentaisit (ja nollaisit) valitun kohteen, voit tallentaa valitun kohteen ID:n:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Laske kaikki renderöinnin aikana
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}

Nyt ei ole tarvetta “säätää” tilaa ollenkaan. Jos kohde valitulla ID:llä on listassa, se pysyy valittuna. Jos ei ole, renderöinnin aikana laskettu selection tulee olemaan null sillä yhtään vastaavaa kohdetta ei löytynyt. Tämä käyttäytyminen erilainen, mutta väitetysti parempi, koska useimmat muutokset items-propsissa säilyttävät valinnan.

Logiikan jakaminen tapahtumakäsittelijöiden kesken

Sanotaan, että sinulla on tuotesivu, jossa on kaksi painiketta (Osta ja Siirry kassalle), jotka molemmat antavat sinun ostaa tuotteen. Haluat näyttää ilmoituksen aina, kun käyttäjä laittaa tuotteen ostoskoriin. showNotification()-funktion kutsuminen molempien painikkeiden klikkaustapahtumankäsittelijöissä tuntuu toistuvalta, joten saatat tuntea houkutuksen laittaa tämä logiikka Effectiin:

function ProductPage({ product, addToCart }) {
// 🔴 Vältä: Tapahtumakohtainen logiikka Effektissa
useEffect(() => {
if (product.isInCart) {
showNotification(`Lisätty ${product.name} ostoskoriin!`);
}
}, [product]);

function handleBuyClick() {
addToCart(product);
}

function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}

Tämä Effekti on turha. Se todennäköisesti tulee aiheuttamaan bugeja. Esimerkiksi, sanotaan, että sovelluksesi “muistaa” ostoskorin sivulatausten välillä. Jos lisäät tuotteen ostoskoriin kerran ja päivität sivua, ilmoitus tulee näkyviin uudestaan. Se tulee näkymään joka kerta, kun päivität tuotteen sivun. Tämä johtuu siitä, että product.isInCart on jo true sivun latauksessa, joten yllä oleva Effekti kutsuu showNotification()-funktiota.

Kun et ole varma, pitäisikö koodin olla Effektissa vai tapahtumankäsittelijässä, kysy itseltäsi miksi tämä koodi täytyy ajaa. Käytä Effektejä vain koodille, joka täytyy ajaa koska komponentti näytettiin käyttäjälle. Tässä esimerkissä ilmoituksen tulisi näkyä koska käyttäjä painoi nappia, ei koska sivu näytettiin! Poista Effekti ja laita jaettu logiikka funktioon, jota kutsutaan molemmista tapahtumankäsittelijöistä:

function ProductPage({ product, addToCart }) {
// ✅ Tapahtumakohtainen logiikka kutsutaan tapahtumankäsittelijöistä
function buyProduct() {
addToCart(product);
showNotification(`Lisätty ${product.name} ostoskoriin!`);
}

function handleBuyClick() {
buyProduct();
}

function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}

Tämä sekä poistaa turhan Effektin sekä korjaa bugin.

POST pyynnön lähettäminen

Tämä Form komponentti lähettää kahdenlaisia POST-pyyntöjä. Se lähettää analytiikkatapahtuman kun se renderöidään. Kun täytät lomakkeen ja painat Lähetä-nappia, se lähettää POST-pyynnön /api/register-päätepisteeseen:

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Tämä logiikka tulisi suorittaa sillä komponentti näytettiin
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

// 🔴 Vältä: Tapahtumakohtainen logiikka Effektissa
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);

function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}

Otetaan käyttöön sama kriteeri kuin edellisessä esimerkissä.

Analytiikka-POST-pyynnön tulisi pysyä Effektissa. Tämä johtuu siitä, että syy lähettää analytiikkatapahtuma on se, että lomake näytettiin. (Se tultaisiin suorittamaan kahdesti kehitysvaiheessa, mutta katso täältä miten hoitaa se.)

Kuitenkin, /api/register POST-pyyntö ei ole aiheutettu lomakkeen näyttämisestä. Haluat lähettää pyynnön vain yhteen tiettyyn aikaan: kun käyttäjä painaa nappia. Se tulisi tapahtua vain tässä tiettynä vuorovaikutuksena. Poista toinen Effekti ja siirrä POST-pyyntö tapahtumankäsittelijään:

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Hyvä: Logiikka suoritetaan, koska komponentti näytettiin
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

function handleSubmit(e) {
e.preventDefault();
// ✅ Hyvä: Tapahtumakohtainen logiikka on tapahtumakäsittelijässä
post('/api/register', { firstName, lastName });
}
// ...
}

Kun valitset laitatko logiikan tapahtumankäsittelijään vai Effektiin, pääkysymys, johon sinun täytyy vastata on minkä tyyppistä logiikkaa se on käyttäjän näkökulmasta. Jos tämä logiikka on aiheutettu tietystä vuorovaikutuksesta, pidä se tapahtumankäsittelijässä. Jos se on aiheutettu käyttäjän näkemisestä komponentin ruudulla, pidä se Effektissä.

Laskutoimitusten ketjutus

Joskus saatat tuntea houkutuksen ketjuttaa Efektejä, jotka kumpikin säätävät tilaa toisen tilan pohjalta:

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);

// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);

useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);

useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);

useEffect(() => {
alert('Good game!');
}, [isGameOver]);

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}

// ...

Tässä koodissa on kaksi ongelmaa.

Ensimmäinen ongelma on, että se on hyvin tehoton: komponentti (ja sen lapset) täytyy renderöidä uudelleen jokaisen set-kutsun välillä ketjussa. Yllä olevassa esimerkissä, pahimmassa tapauksessa (setCard → renderöi → setGoldCardCount → renderöi → setRound → renderöi → setIsGameOver → renderöi) on kolme tarpeetonta uudelleenrenderöintiä puussa.

Vaikka se ei olisi hidas, koodisi eläessä tulet törmäämään tilanteisiin, joissa “ketju” jonka kirjoitit, ei vastaa uusia vaatimuksia. Kuvittele, että olet lisäämässä tapaa selata pelin siirtohistoriaa. Tämä tehdään päivittämällä jokainen tilamuuttuja arvoon menneisyydestä. Kuitenkin, card-tilan asettaminen menneisyyden arvoon aiheuttaisi Efektiketjun uudelleen ja muuttaisi näytettävää dataa. Tällainen koodi on usein jäykkää ja haurasta.

Tässä tilanteessa on parempi laskea mitä voit renderöinnin aikana ja säätää tilaa tapahtumankäsittelijässä:

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);

// ✅ Lakse mitä voit renderöinnin aikana
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}

// ✅ Laske koko seuraava tila tapahtumakäsittelijässä
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}

// ...

Tämä on paljon tehokkaampaa. Myöskin, jos toteutat tavan katsoa siirtohistoriaa, voit nyt asettaa jokaisen tilamuuttujan menneisyyden arvoon käynnistämättä Efektiketjua, joka säätää jokaista muuta arvoa. Jos tarvitset uudelleenkäytettävää logiikkaa useiden tapahtumankäsittelijöiden välillä, voit irroittaa funktion ja kutsua sitä näistä käsittelijöistä.

Muista, että tapahtumankäsittelijöissä tila käyttäytyy kuin tilannekuva. Esimerkiksi, vaikka kutsuisit setRound(round + 1), round-muuttuja heijastaa arvoa siihen aikaan, kun käyttäjä painoi nappia. Jos tarvitset seuraavan arvon laskutoimituksiin, määrittele se manuaalisesti kuten const nextRound = round + 1.

Joissain tapauksissa, et voi laskea seuraavaa tilaa suoraan tapahtumankäsittelijässä. Esimerkiksi, kuvittele lomake, jossa on useita alasvetovalikoita, joiden seuraavat vaihtoehdot riippuvat edellisen alasvetovalikon valitusta arvosta. Tällöin Efektiketju on sopiva, koska synkronoit verkon kanssa.

Sovelluksen alustaminen

Osa logiikasta tulisi suorittaa kerran kun sovellus alustetaan.

Saatat tuntea houkutuksen laittaa se Effektiin pääkomponenttiin:

function App() {
// 🔴 Vältä: Effektit logiikalla, joka tulisi suorittaa vain kerran
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}

Kuitenkin, tulet nopeasti huomaamaan, että se suoritetaan kahdesti kehitysvaiheessa. Tämä voi aiheuttaa ongelmia—esimerkiksi, se voi mitätöidä autentikointitokenin, koska funktio ei ollut suunniteltu kutsuttavaksi kahdesti. Yleisesti ottaen, komponenttiesi tulisi olla joustavia uudelleenmounttaukselle. Pääkomponenttisi mukaanlukien.

Vaikka sitä ei välttämättä koskaan uudelleenmountata käytännössä tuotannossa, samojen rajoitteiden noudattaminen kaikissa komponenteissa tekee koodin siirtämisestä ja uudelleenkäytöstä helpompaa. Jos jotain logiikkaa täytyy suorittaa kerran sovelluksen latauksessa sen sijaan, että se suoritettaisiin kerran komponentin mounttauksessa, lisää pääkomponenttiin muuttuja, joka seuraa onko se jo suoritettu:

let didInit = false;

function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Suoritetaan vain kerran sovelluksen alustuksessa
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}

Voit myös suorittaa sen moduulin alustuksen aikana ja ennen kuin sovellus renderöidään:

if (typeof window !== 'undefined') { // Tarkista, olemmeko selaimessa.
// ✅ Suoritetaan vain kerran alustuksessa
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

Ylätasolla oleva koodi suoritetaan kerran kun komponenttisi importataan—vaikka sitä ei tultaisi renderöimään. Välttääksesi hidastumista tai yllättävää käytöstä importatessa satunnaisia komponentteja, älä käytä tätä mallia liikaa. Pidä sovelluksen laajuinen alustuslogiikka juurikomponenttimoduuleissa kuten App.js tai sovelluksesi sisäänkäynnissä.

Tilamuutosten ilmoittaminen pääkomponentille

Sanotaan, että olet kirjoittamassa Toggle komponenttia sisäisellä isOn tilalla, joka voi olla joko true tai false. On muutamia eri tapoja asettaa se (klikkaamalla tai raahaamalla). Haluat ilmoittaa pääkomponentille aina kun Toggle:n sisäinen tila muuttuu, joten paljastat onChange tapahtuman ja kutsut sitä Efektistä:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

// 🔴 Vältä: onChange käsittelijä suoritetaan myöhässä
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])

function handleClick() {
setIsOn(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}

// ...
}

Kuten aiemmin, tämä ei ole ihanteellista. Toggle päivittää tilansa ensin ja React päivittää näytön. Sitten React suorittaa Efektin, joka kutsuu onChange funktiota, joka on välitetty pääkomponentilta. Nyt pääkomponentti päivittää oman tilansa, aloittaen toisen renderöintikierroksen. Olisi parempi tehdä kaikki yhdellä kierroksella.

Poista Effekti ja päivitä sen sijaan molempien komponenttien tila samassa tapahtumankäsittelijässä:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

function updateToggle(nextIsOn) {
// ✅ Hyvä: Suorita kaikki päivitykset tapahtuman aikana
setIsOn(nextIsOn);
onChange(nextIsOn);
}

function handleClick() {
updateToggle(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}

// ...
}

Tällä lähestymistavalla sekä Toggle komponentti että sen pääkomponentti päivittävät tilansa tapahtuman aikana. React pakkaa päivitykset eri komponenteista yhteen, joten renderöintikierroksia on vain yksi.

Saatat myös pystyä poistamaan tilan kokonaan ja vastaanottamaan isOn arvon pääkomponentilta:

// ✅ Myös hyvä: komponentti on täysin kontrolloitu sen pääkomponentin toimesta
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}

// ...
}

“Nostamalla tilan ylös” voit täysin kontrolloida Toggle:n tilaa pääkomponentista vaihtamalla pääkomponentin omaa tilaa. Tämä tarkoittaa, että pääkomponentin täytyy sisältää enemmän logiikkaa, mutta vähemmän tilaa yleisesti ottaen. Aina kun yrität pitää kaksi eri tilamuuttujaa synkronoituna, kokeile nostaa tila ylös sen sijaan!

Tiedon välittäminen pääkomponentille

Tämä Child komponentti hakee dataa ja välittää sen sitten Parent komponentille Efektissä:

function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Vältä: Välitetään dataa pääkomponenille Effektissa
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}

Reactissa data kulkee pääkomponentista sen alakomponenteille. Kun näet jotain väärin ruudulla, voit jäljittää mistä tiedot tulevat menemällä ylöspäin komponenttiketjussa kunnes löydät komponentin, joka välittää väärän propin tai jolla on väärä tila. Kun alakomponentit päivittävät pääkomponenttien tilaa Efekteissä, tiedon virtaus on hyvin vaikea jäljittää. Koska sekä alakomponentti että pääkomponentti tarvitsevat samat tiedot, anna pääkomponentin hakea tiedot ja välitä ne alakomponentille sen sijaan:

function Parent() {
const data = useSomeAPI();
// ...
// ✅ Hyvä: Välitetään dataa alakomponentille
return <Child data={data} />;
}

function Child({ data }) {
// ...
}

Tämä on yksinkertaisempaa ja pitää datavirran ennustettavana: data virtaa alaspäin pääkomponentilta alakomponentille.

Tilaaminen ulkoiseen varastoon

Joskus komponenttisi saattavat tarvita tilata dataa Reactin ulkopuolelta. Tämä data voisi olla kolmannen osapuolen kirjastosta tai selaimen sisäänrakennetusta API:sta. Koska tämä data voi muuttua Reactin tietämättä, sinun täytyy manuaalisesti tilata komponenttisi siihen. Tämä tehdään usein Efektillä, esimerkiksi:

function useOnlineStatus() {
// Ei ideaali: Manuaalinen tietovaraston tilaus Efektissä
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}

updateState();

window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Tässä komponentti tilaa ulkoisen tietovaraston (tässä tapauksessa selaimen navigator.onLine APIn). Koska tätä APIa ei ole olemassa palvelimella (joten sitä ei voi käyttää alustavaan HTML:ään), alustetaan tila aluksi true:ksi. Aina kun tietovaraston arvo muuttuu selaimessa, komponentti päivittää tilansa.

Vaikka on yleistä käyttää Effektia tähän, React sisältää tietovaraston tilaukseen tarkoitukseen tehdyn Hookin, jota suositellaan sen sijaan. Poista Efekti ja korvaa se useSyncExternalStore kutsulla:

function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);

return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

function useOnlineStatus() {
// ✅ Hyvä: Tilataan ulkoinen varasto Reactin sisäänrakennetulla Hookilla
return useSyncExternalStore(
subscribe, // React ei tilaa uudelleen niin kauan kuin välität saman funktion
() => navigator.onLine, // Miten arvo haetaan selaimella
() => true // Miten arvo haetaan palvelimella
);
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Tämä lähestymistapa on vähemmän virhealtis kuin muuttuvan datan manuaalinen synkronointi Reactin tilaan Efektillä. Yleensä kirjoitat oman Hookin kuten useOnlineStatus() yllä, jotta sinun ei tarvitse toistaa tätä koodia yksittäisissä komponenteissa. Lue lisää ulkoisten varastojen tilaamisesta React komponenteista.

Tiedon haku

Moni sovellus käyttää effekteja datan hakemiseen. On hyvin yleistä kirjoittaa datan hakemiseen tarkoitettu efekti näin:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);

useEffect(() => {
// 🔴 Vältä: Tiedon hakeminen ilman siivouslogiikkaa
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

Sinun ei tarvitse siirtää tätä hakua tapahtumankäsittelijään.

Tämä saattaa kuulostaa ristiriitaiselta edellisten esimerkkien kanssa, jossa sinun täytyi asettaa logiikka tapahtumakäsittelijöihin! Kuitenkin, harkitse, että kirjoitustapahtuma ei ole itse pääsyy hakemiseen. Hakusyötteet ovat usein esitäytetty URL:stä, ja käyttäjä saattaa navigoida takaisin ja eteenpäin ilman että koskee syötteeseen.

Ei ole väliä mistä page ja query tulevat. Vaikka tämä komponentti on näkyvissä, saatat haluta pitää results tilan synkronoituna verkon datan kanssa nykyiselle pagelle ja querylle. Tämän takia se on Efekti.

Kuitenkin, yllä olevassa koodissa on bugi. Kuvittele, että kirjoitat "moikka" todella nopeasti. Sitten query muuttuu ensin "m", josta "mo", "moi", "moik", "moikk", and "moikka". Tämä käynnistää useita hakuja, muta ei ole takuita siitä missä järjestyksessä vastaukset tulevat. Esimerkiksi, "moik" vastaus saattaa saapua "moikka" vastauksen jälkeen. Koska se kutsuu lopuksi setResults():a, väärät hakutulokset tulevat näkyviin. Tätä kutsutaan englanniksi “race condition”: kaksi eri pyyntöä “kilpailivat” toisiaan vastaan ja tulivat eri järjestyksessä kuin odotit.

Korjataksesi race conditioniin, sinun täytyy lisätä siivousfunktio, joka jättää huomiotta vanhentuneet vastaukset:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

Tämä varmistaa, että kun Efekti hakee dataa, kaikki vastaukset paitsi viimeisin pyydetty jätetään huomiotta.

Race conditionien käsitteleminen ei ole ainoa vaikeus datan hakemisessa. Saatat myös haluta miettiä vastausten välimuistitusta (jotta käyttäjä voi klikata takaisin ja nähdä edellisen näytön välittömästi), miten hakea dataa palvelimella (jotta alustava palvelimella renderöity HTML sisältää haetun sisällön sen sijaan että näyttäisi latausikonia), ja miten välttää verkon vesiputoukset (jotta alakomponentti voi hakea dataa ilman että odottaa jokaista vanhempaa).

Nämä ongelmat pätevät mihin tahansa käyttöliittymäkirjastoon, ei vain Reactiin. Niiden ratkaiseminen ei ole triviaalia, minkä takia modernit ohjelmistokehykset tarjoavat tehokkaampia sisäänrakennettuja datan hakumekanismeja kuin datan hakeminen effekteissä.

Jos et käytä ohjelmistokehitystä (ja et halua rakentaa omaasi), mutta haluat tehdä datan hakemisesta effekteistä ergonomisempaa, harkitse hakulogiiikan eristämistä omaksi Hookiksi kuten tässä esimerkissä:

function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}

Todennäköisesti haluat myös lisätä logiikkaa virheiden käsittelyyn ja seurata onko sisältö latautumassa. Voit rakentaa Hookin kuten tämän itse tai käyttää yhtä monista ratkaisuista, jotka ovat jo saatavilla React-ekosysteemissä. Vaikka tämä yksinään ei ole yhtä tehokas kuin ohjelmistokehyksen sisäänrakennettu datan hakumekanismi, datan hakulogiikan siirtäminen omaan Hookiin tekee tehokkaan datan hakustrategian käyttöönotosta helpompaa myöhemmin.

Yleisesti ottaen aina kun joudut turvautumaan Effectien kirjoittamiseen, pidä silmällä milloin voit eristää toiminnallisuuden omaksi Hookiksi, jolla on deklaratiivisempi ja tarkoitukseen sopivampi API kuten useData yllä. Mitä vähemmän raakoja useEffect-kutsuja sinulla on komponenteissasi, sitä helpompaa sinun on ylläpitää sovellustasi.

Kertaus

  • Jos voit laskea jotain renderöinnin aikana, et tarvitse Effectiä.
  • Välimuistittaaksesi kalliit laskelmat, lisää useMemo useEffectn sijaan.
  • Nollataksesi kokonaisen komponenttipuun tilan, välitä eri key propsi komponentille.
  • Nollataksesi tilan propsin muutoksen jälkeen, aseta se renderöinnin aikana.
  • Koodi joka suoritetaan koska komponentti näytetään tulisi olla Efekteissä, muu koodi tapahtumissa.
  • Jos sinun täytyy päivittää useamman kuin yhden komponentin tila, on parempi tehdä se yhden tapahtuman aikana.
  • Aina kun sinun täytyy synkronoida tila useiden komponenttien välillä, harkitse tilan nostamista ylös.
  • Voit hakea dataa Effekteissa, mutta sinun täytyy toteuttaa siivousfuktio välttääksesi kilpailutilanteet.

Haaste 1 / 4:
Muunna dataa ilman Effektejä

TodoList komponentti alla näyttää listan tehtävistä. Kun “Show only active todos” valintaruutu on valittuna, valmiita tehtäviä ei näytetä listassa. Riippumatta siitä mitkä tehtävät ovat näkyvissä, alatunniste näyttää tehtävien määrän, jotka eivät ole vielä valmiita.

Yksinkertaista tämä komponentti poistamalla turha tila ja Effektit.

import { useState, useEffect } from 'react';
import { initialTodos, createTodo } from './todos.js';

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [activeTodos, setActiveTodos] = useState([]);
  const [visibleTodos, setVisibleTodos] = useState([]);
  const [footer, setFooter] = useState(null);

  useEffect(() => {
    setActiveTodos(todos.filter(todo => !todo.completed));
  }, [todos]);

  useEffect(() => {
    setVisibleTodos(showActive ? activeTodos : todos);
  }, [showActive, todos, activeTodos]);

  useEffect(() => {
    setFooter(
      <footer>
        {activeTodos.length} todos left
      </footer>
    );
  }, [activeTodos]);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={showActive}
          onChange={e => setShowActive(e.target.checked)}
        />
        Show only active todos
      </label>
      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
      {footer}
    </>
  );
}

function NewTodo({ onAdd }) {
  const [text, setText] = useState('');

  function handleAddClick() {
    setText('');
    onAdd(createTodo(text));
  }

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={handleAddClick}>
        Add
      </button>
    </>
  );
}