A Hatás Horog
A Horgok a React egy új kiegészítése a 16.8-as verziótól kezdve. Lehetővé teszik számodra állapotok és más React funkciók használatát osztályok írása nélkül.
A Hatás Horog segítségével mellékhatásokat tudsz elvégezni egy függvénykomponensben:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Hasonló a componentDidMount-hoz és a componentDidUpdate-hez: useEffect(() => { // A böngésző API segítségével frissíti a dokumentum címét document.title = `${count} alkalommal kattintottál`; });
return (
<div>
<p>{count} alkalommal kattintottál</p>
<button onClick={() => setCount(count + 1)}>
Kattints rám
</button>
</div>
);
}
Ez a kódrészlet az előző oldali számláló példára épül, de hozzáadtunk egy új funkciót is: a dokumentum címét átállítottuk egy általunk írt üzenetre, amiben az van benne, hogy hányszor kattintottak.
Az Adatlekérés, feliratkozás, vagy a DOM manuális frissítése React komponensekben mind példa mellékhatásokra. Még ha nem is szoktad ezeket “mellékhatásoknak” (vagy csak “hatásoknak”) hívni, valószínű, hogy már használtad őket a komponenseidben.
Tipp
Ha már ismered a React osztályok életciklus metódusait, a
useEffect
Horogra gondolhatsz úgy is, mint acomponentDidMount
,componentDidUpdate
, éscomponentWillUnmount
egyesítése.
Kétféle gyakori mellékhatás létezik React komponensekben: azok, amiknél nincs szükség takarításra, és azok, amiknél van. Most nézzük meg részletesebben, hogy mi ezek között a különbség.
Takarítást nem igénylő hatások
Néha csak azután szeretnénk egy kódrészletet futtatni, miután a React frissítette a DOM-ot. Hálózati kérések, manuális DOM manipulálás vagy logolás mind példák arra, amik nem igényelnek takarítást. Ezt azért mondjuk, mert ezeket futtatás után el is felejthetjük. Most hasonlítsuk össze, hogy hogyan tudjuk kifejezni a mellékhatásokat osztályok vagy Horgok segítségével.
Példa osztályok használatával
A React osztályokban a render
metódus önmagában nem kéne, hogy mellékhatásokat okozzon. Ez túl korai lenne — általában azután szeretnénk a hatásokat végrehajtani, hogy a React frissítette a DOM-ot.
Emiatt a React osztályokban a mellékhatásokat a componentDidMount
-ba és a componentDidUpdate
-be helyezzük. Visszatérve a példánkhoz, itt van egy React számláló osztály, ami frissíti a dokumentum címét rögtön miután a React frissítette a DOM-ot:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() { document.title = `${this.state.count} alkalommal kattintottál`; } componentDidUpdate() { document.title = `${this.state.count} alkalommal kattintottál`; }
render() {
return (
<div>
<p>{this.state.count} alkalommal kattintottál</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Kattints rám
</button>
</div>
);
}
}
Figyeld meg, hogy duplikálnunk kell a kódot ebben a két életciklus metódusban.
Ez azért van, mert sok esetben ugyanazt a mellékhatást szeretnénk végrehajtani attól függetlenül, hogy a komponens most lett létrehozva, vagy csak frissítve lett. Alapvetően ezt minden renderelés után szeretnénk végrehajtani — de a React osztálykomponenseknek nincs ilyen metódusa. Kiemelhetnénk egy külön metódusba, de még így is két helyről kéne azt meghívni.
Most nézzük, hogy hogyan tudjuk ugyanezt a useEffect
Horoggal elérni.
Példa Horgok használatával
Már láttuk ugyanezt a példát az oldal tetején, de nézzük meg még egyszer:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `${count} alkalommal kattintottál`; });
return (
<div>
<p>{count} alkalommal kattintottál</p>
<button onClick={() => setCount(count + 1)}>
Kattints rám
</button>
</div>
);
}
Mi csinál a useEffect
? Ennek a Horognak a használatával megmondhatod a Reactnek, hogy a komponensednek valamit csinálnia kell a renderelés után. A React megjegyzi az átadott függvényt (erre majd úgy hivatkozunk, mint a “hatás”) és később, a DOM frissítések után meghívja ezt. Ebben a hatásban beállítjuk a dokumentum címét, de akár adatlekérést vagy valami más API hívást is elvégezhetnénk.
Miért egy komponensen belül hívjuk meg a useEffect
-et? A useEffect
komponensen belül való elhelyezése megengedi, hogy a count
állapotváltozót (vagy bármelyik másik propot) elérjük közvetlenül a hatásból. Nem kell egy speciális API, hogy kiolvassuk — ez már benne van a függvény hatókörében. A Horgok a JavaScript closure-öket használják ahelyett, hogy React-specifikus API-kat hoznának létre, mivel a JavaScriptben már van erre megoldás.
Lefut-e a useEffect
mind renderelés után? Igen! Alapvetően az első rendereléskor és minden egyes frissítés után is lefut. (Majd később arról is beszélünk, hogy hogyan szabjuk személyre ezt.) Ahelyett, hogy “létrehozás”-ban és “frissítés”-ban gondolkozunk, egyszerűbb lehet, ha úgy gondolunk rá, hogy a hatások “renderelés után” történnek. A React garantálja, hogy a DOM már frissült, amikor a hatásokat futtatja.
Részletes magyarázat
Most, hogy már többet tudunk a hatásokról, ezek a sorok több értelmet nyertek:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `${count} alkalommal kattintottál`;
});
}
Deklaráljuk a count
állapotváltozót és megmondjuk a Reactnek, hogy egy hatást kell végrehajtanunk. Átadunk egy függvényt a useEffect
Horognak. A függvény, amit átadunk lesz a hatásunk. A hatásunkon belül beállítjuk a dokumentum címét a document.title
böngésző API-val. A hatáson belül ki tudjuk olvasni a legfrissebb count
értéket, mivel ez a függvényünk hatókörében van. Amikor a React rendereli a komponensünket, emlékezni fog a megadott hatásra, és lefuttatja ezt a DOM frissítése után. Ez minden rendereléskor megtörténik az elsőt is beleértve.
Gyakorlottabb JavaScript fejlesztők észrevehetik, hogy a useEffect
-nek átadott függvény minden rendereléskor különböző lesz. Ez szándékosan van így. Valójában emiatt tudjuk kiolvasni a count
értékét a hatáson belül anélkül, hogy aggódnunk kéne a frissessége miatt. Minden egyes újrarendereléskor egy másik hatást ütemezünk be felülírva az előzőt. Bizonyos tekintetben emiatt úgy tűnhet, hogy a hatás a renderelés eredményének a részeként viselkedik — minden egyes hatás egy adott rendereléshez “tartozik”. A későbbieknek látni fogjuk, hogy ez miért is hasznos.
Tipp
A
componentDidMount
-tal és acomponentDidUpdate
-tel ellentétben auseEffect
-tel ütemezett hatások nem blokkolják a képernyőfrissítést. Emiatt az appod gyorsabbnak tűnhet. A legtöbb hatásnak nem kell szinkron történnie. Néhány kivételes esetben (például a felhasználói felület lemérése), amikor igen, van egy másikuseLayoutEffect
Horog, aminek az API-ja ugyanaz, mint auseEffect
-nek.
Takarítást igénylő hatások
Korábban megnéztük, hogy hogyan fejezzünk ki takarítást nem igénylő mellékhatásokat. Azonban néhány hatásnak szüksége van rá. Például, amikor feliratkozunk egy külső adatfolyamra. Ebben az esetben fontos, hogy takarítást végezzünk, hogy ne okozzunk memóriaszivárgást! Hasonlítsuk össze, hogyan tudjuk ezt osztályokkal és Horgokkal megtenni.
Példa osztályok használatával
Egy React osztályban tipikusan a componentDidMount
-ban állítanál be egy feliratkozást és a componentWillUnmount
-ban iratkoznál le. Például, ha mondjuk van egy ChatAPI
modulunk amivel feliratkozhatunk egy barátunk online állapotára. Egy osztállyal így tudnánk feliratkozni és kiírni az állapotát:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
Figyeld meg, hogy a componentDidMount
és a componentWillUnmount
egymás tükörképe. Az életciklus metódusok arra kényszerítenek minket, hogy ezt a logikát felosszuk különböző metódusok között, habár ezek fogalmilag ugyanahhoz a hatáshoz tartoznak.
Megjegyzés
A sasszemű olvasók észrevehették, hogy egy
componentDidUpdate
metódus is kéne ahhoz, hogy ez a példa teljesen korrekt legyen. Ezt most figyelmen kívül hagyjuk, de a későbbiekben visszatérünk erre.
Példa Horgok használatával
Nézzük, hogy hogyan tudjuk ezt a komponenst átírni Horgok használatával.
Azt gondolhatnád, hogy egy külön hatás kell majd a takarításhoz. De a le- és feliratkozás kódja annyira összefügg, hogy a useEffect
-et úgy terveztük, hogy ezeket egy helyen tarthassuk. Ha a hatásod egy függvénnyel tér vissza, a React a takarításkor fogja meghívni ezt:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Itt add meg, hogy hogyan takarítson ki a hatás lefutása után: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Miért tértünk vissza egy függvénnyel a hatásból? Ez az optimális takarítási mechanizmus a hatásokban. Minden hatás visszaadhat egy takarító függvényt. Így a fel- és leiratkozáshoz használt kódot egy helyen tárolhatjuk. Ezek ugyanahhoz a hatáshoz tartoznak!
Pontosan mikor takarít fel a React egy hatást? A React akkor takarít, amikor a komponens leválasztódik. Azonban, ahogy korábban megtanultuk, a hatások minden renderelésről lefutnak, és nem csak egyszer. Emiatt a React minden egyes rendereléskor kitakarít az előző renderelés után, mielőtt az új hatásokat futtatja. Később megbeszéljük, ez miért is segít elkerülni a bugokat és hogyan tiltsuk le ezt a viselkedést a teljesítményproblémák elkerülése végett.
Megjegyzés
Nem kell, hogy elnevezzük a függvényt, amivel visszatérünk a hatásból. Mi
cleanup
-nak (takarításnak) hívtuk itt, hogy egyértelmű legyen a célja, de egy nyílfüggvényt is visszaadhatsz vagy máshogy is hívhatod.
Összefoglalás
Megtanultuk, hogy a useEffect
-tel kifejezhetünk különbözőféle hatásokat amik a renderelés után futnak le. Néhány hatásnak szüksége lehet takarításra, így ezek egy függvénnyel térnek vissza:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
Más hatásoknak nem feltétlenül van szüksége erre, így ezek nem térnek vissza semmivel.
useEffect(() => {
document.title = `${count} alkalommal kattintottál`;
});
A Hatás Horog API-ja mindkét esetet lefedi.
Ha úgy érzed, hogy már elég jól érted a Hatás Horgok működését, vagy ha ez most túl sok volt, átugorhatsz a következő oldalra, ami a Horgok szabályairól szól.
Tippek a Hatások használatához
Az következő részben részletesebben kielemezzük a useEffect
néhány tulajdonságát, ami a gyakorlottabb React felhasználók számára érdekes lehet. Ne érezd azt, hogy ezt most kötelező elolvasnod. A későbbiekben is visszatérhetsz erre az oldalra, hogy többet megtudj a Hatás Horog részleteiről.
Tipp: Használj különálló hatásokat különböző funkciók megvalósítására
Az egyik probléma amit a Horgok Motivációja részben kiemeltünk az az, hogy az osztály életciklus metódusok gyakran tartalmaznak egymással össze nem illő logikát, amíg összetartozó logika gyakran több metódusba osztódik fel. Íme egy komponens ami a számláló és a barát online státusz mutatójának logikáját vegyíti az előző példákból:
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `${this.state.count} alkalommal kattintottál`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `${this.state.count} alkalommal kattintottál`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
Figyeld meg, hogy a logika, ami beállítja a document.title
-t, szét van osztva a componentDidMount
és a componentDidUpdate
között. A feliratkozási logika pedig szét van osztva a componentDidMount
és a componentWillUnmount
között. A componentDidMount
-ben mindkét logikából van kód.
Szóval hogyan tudják a Horgok megoldani ezt a problémát? Mint ahogy az Állapot Horgot többször is fel tudod használni, ugyanúgy többféle hatást is használhatsz. Ez lehetőséget ad rá, hogy a különböző funkciókat külön hatásokba írjuk.
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => { document.title = `${count} alkalommal kattintottál`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => { function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
A Horgok használatával viselkedés alapján tudjuk felosztani a kódot, ahelyett, hogy az életciklusok nevei alapján tennénk ezt. A React az összes hatást végrehajtja a komponensben a specifikáció sorrendjében.
Magyarázat: Miért futnak le a hatások minden egyes frissítéskor
Ha az osztályokhoz vagy szokva, elgondolkozhattál rajta, hogy a hatások takarítása miért történik meg minden újrarendereléskor, és nem csak akkor, amikor a komponens leválasztódik. Nézzünk meg egy gyakorlati példát arról, hogy ez a viselkedés miért segít nekünk hibamentesebb komponenseket írni.
Fentebb ezen az oldalon bemutattunk egy FriendStatus
komponens példát, ami megjeleníti egy barátunk online állapotát. Az osztályunk kiolvassa a friend.id
-t a this.props
-ból, feliratkozik a barátunk állapotára a komponens létrejötte után és leiratkozik a leválasztáskor:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
De mi történik akkor, ha a friend
prop megváltozik, miközben a komponens épp látszik a felületen? A komponensünk továbbra is az előző barát állapotát jelenítené meg. Ez egy bug. Ez memóriaszivárgást vagy az alkalmazás összeomlását is okozhatná, mivel a leiratkozási metódus rossz ID-val hívódik meg.
Egy osztálykomponensben hozzá kéne adnunk a componentDidUpdate
-t, hogy ezt az esetet is tudjuk kezelni:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) { // Leiratkozás az előző friend.id-ról ChatAPI.unsubscribeFromFriendStatus( prevProps.friend.id, this.handleStatusChange ); // Feliratkozás a következő friend.id-re ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
A componentDidUpdate
rossz használta egy gyakori hibaforrás React alkalmazásokban.
Most nézzük meg ezt a verziót, ami Horgokat használ:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
Ebben nincs benne ugyanaz a hiba. (De nem is változtattunk semmit rajta.)
Nincs szükség külön kódra, ami a frissítéseket kezeli, mivel a useEffect
alapból lekezeli ezeket. Ez kitakarítja az előző hatásokat, mielőtt az új lefutna. Hogy ezt illusztráljuk, itt van egy sorozat a feliratkozási és leiratkozási hívásokból:
// Létrehozás a { friend: { id: 100 } } propokkal
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Az első hatás futtatása
// Frissítés { friend: { id: 200 } } propokkalí
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Az előző hatás kitakarítása
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // A következő hatás futtatása
// Frissítés { friend: { id: 300 } } propokkal
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Az előző hatás kitakarítása
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // A következő hatás futtatása
// Leválasztás
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Az utolsó hatás kitakarítása
Ez a viselkedés alapból biztosítja a konzisztenciát és megelőzi a hibákat, amik gyakoriak az osztálykomponensekben az elfelejtett logika miatt.
Tipp: A teljesítmény optimalizálása a hatások elhagyásával
Néhány esetben teljesítményproblémákat okozhat a minden renderelés utáni hatások végrehajtása és azok takarítása. Az osztálykomponensekben ezt megoldhatjuk a prevProps
és prevState
összehasonlításával a componentDidUpdate
-ben belül:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `${this.state.count} alkalommal kattintottál`;
}
}
Ez a követelmény elég gyakori, így bele lett építve a useEffect
Horog API-jába is. Megmondhatod a Reactnek, hogy hagyja ki ezt a hatást, ha bizonyos értékek nem változtak az újrarenderelés során. Ehhez a useEffect
-nek második paraméterként egy tömböt kell átadni:
useEffect(() => {
document.title = `${count} alkalommal kattintottál`;
}, [count]); // Csak akkor futtatja újra a hatást, ha a count megváltozott
A fenti példában a [count]
-ot egy második paraméterként adjuk át. Ez mit jelent? Ha a count
-nak 5
az értéke, akkor a komponensünk újrarenderelődik a count
változatlan értékével, ami 5
, A React összehasonlítja az [5]
-öt az előző renderelésből és az [5]
-öt a következő renderelésből. Mivel a tömb összes eleme megegyezik (5 === 5
), a React kihagyja a hatást. Ez a mi optimalizálásunk.
Amikor a count
értéke 6
-ra frissül, a React összehasonlítja az előző renderelésbeli [5]
tömb elemeit a következő renderelésbeli [6]
tömbből. A mostani esetben a React újra lefuttatja a hatást, mivel 5 !== 6
. Ha több eleme is van a tömbnek, a React akkor is újrafuttatja a hatást, ha csak egy elem különbözik.
Ez akkor is működik, ha a hatásnak van egy takarítási fázisa is:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Csak akkor iratkozik fel újra, ha a props.friend.id megváltozott
Elképzelhető, hogy a jövőben ez a második argumentum automatikusan lesz hozzáadva egy fordítási transzformációval.
Megjegyzés
Ha ezt az optimalizálást használod, bizonyosodj meg róla, hogy összes érték benne van a komponens hatóköréből (mint a propok és az állapot), amik az idő során változnak, és amit a hatás használ. Enélkül a kódod elavult értékeket fog tartalmazni az előző renderelésekből. Tudj meg többet arról, hogy hogyan kezeld a függvényeket, és arról, hogy mit csináljunk, ha a tömb túl gyakran változik.
Ha egy hatást csak egyszer szeretnél futtatni és kitakarítani (létrehozáskor és leválasztáskor), átadhatsz egy üres tömböt (
[]
) második argumentumként. Ez megmondja a Reactnek, hogy a hatásod nem függ semelyik props vagy állapot értékétől, így ezt nem kell újrafuttatnia. Ez nem egy speciális eset — abból következik, ahogyan a függőségek amúgy is működnek.Ha egy üres tömböt adsz át (
[]
), a propok és állapotváltozók a hatáson belül végig megtartják a kezdeti értéküket. Míg a[]
átadása második paraméterként közelebb áll acomponentDidMount
és acomponentWillUnmount
már ismert modelljéhez, általában jobb megoldások vannak arra, hogy hogyan tudjuk megelőzni a hatások túl gyakori futását. Ne felejts el azt sem, hogy a React késlelteti auseEffect
lefuttatását a böngésző kirajzolási fázisa utánra, így ez az extra munka kevésbé jelent problémát.Ajánljuk az
exhaustive-deps
szabály használatát, ami része azeslint-plugin-react-hooks
csomagnak. Ez jelzi, ha a függőségek rosszul vannak megadva, és megoldást javasol.
Következő lépések
Gratulálunk! Ez egy hosszú oldal volt, de remélhetőleg a végére a legtöbb kérdésedet megválaszoltuk, ami felvetődött a hatások kapcsán. Mind az Állapot Horogról, mind a Hatás Horogról tanultál, és ezek kombinálásával sok mindent el tudsz érni. Ezek lefedik a legtöbb dolgot, amit az osztályoknál is használtunk — és amit nem, azt megtalálhatod a további Horgok között.
Azt is elkezdtük észrevenni, hogy a Horgok hogyan oldják meg a problémákat, amiről a Motiváció részben írtunk. Láttuk, hogy a takarítási mechanizmus hogyan segít elkerülni a componentDidUpdate
-ben és a componentWillUnmount
-ben duplikált kódot, összehozza az összetartozó kódrészeket és segít elkerülni a hibákat. Azt is láttuk, hogy hogyan tudjuk különválasztani a hatásokat céljaik szerint, ami egy olyan dolog, amit az osztályokkal nem tudtunk megtenni.
Ezen a ponton érdekelhet, hogy hogyan működnek a Horgok. Hogyan tudja a React, hogy melyik useState
hívás melyik állapotváltozóhoz tartozik az újrarendereléskor? Hogyan “kapcsolja össze” az előző és következő hatásokat minden frissítéskor? A következő oldalon a Horgok szabályairól fogunk tanulni — ezek elengedhetetlenek a Horgok helyes használatához.