Reaktiivisten Efektien elinkaari

Efekteilla on eri elinkaari komponenteista. Komponentit voivat mountata, päivittyä, tai un-mountata. Efekti voi tehdä vain kaksi asiaa: aloittaa synkronoimaan jotain, ja myöhemmin lopettaa synkronointi. Tämä sykli voi tapahtua useita kertoja, jos Efekti riippuu propseista ja tilasta, jotka muuttuvat ajan myötä. React tarjoaa linter-säännön, joka tarkistaa, että olet määrittänyt Efektin riippuvuudet oikein. Tämä pitää Efektisi synkronoituna viimeisimpiin proppseihin ja statukseen.

Tulet oppimaan

  • Miten Efektin elinkaari eroaa komponentin elinkaaresta
  • Miten ajatella jokaista yksittäistä Efektia erillään
  • Milloin Efektisi täytyy synkronoida uudelleen ja miksi
  • Miten Effektisi riippuvuudet määritellään
  • Mitä tarkoittaa kun arvo on reaktiivinen
  • Mitä tyhjä riippuvuustaulukko tarkoittaa
  • Miten React tarkistaa rippuuksien oikeudellisuuden linterin avulla
  • Mitä tehdä kun olet eri mieltä linterin kanssa

Efektin elinkaari

Jokainen React komponentti käy läpi saman elinkaaren:

  • Komponentti mounttaa kun se lisätään näytölle.
  • Komponentti päivittyy kun se saa uudet propsit tai tilan, yleensä vuorovaikutuksen seurauksena.
  • Komponentti unmounttaa kun se poistetaan näytöltä.

Tämä on hyvä tapa ajatella komponentteja, mutta ei Efektejä. Sen sijaan, yritä ajatella jokaista Efektiä erillään komponentin elinkaaresta. Efekti kuvaa miten ulkoinen järjestelmä synkronoidaan nykyisten propsien ja tilan kanssa. Kun koodisi muuttuu, synkronointi täytyy tapahtua useammin tai harvemmin.

Kuvallistaaksemme tämän pointin, harkitse tätä Efektia, joka yhdistää komponenttisi chat-palvelimeen:

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

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

Efektisi runko määrittää miten syknronointi aloitetaan:

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

Efektisi palauttama siivousfunktio määrittelee miten synkronointi lopetetaan:

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

Intuitiivisesti, saatat ajatella Reactin aloittavan synkronoinnin kun komponenttisi mountataan ja lopettavan synkronoinnin kun komponenttisi unmountataan. Tämä ei kuitenkaan ole tilanne! Joksus, saattaa olla tarpeellista aloittaa ja lopettaa synkronointi useita kertoja kun komponentti pysyy mountattuna.

Katsotaan miksi tämä on tarpeellista, milloin se tapahtuu_, ja miten voit hallita sen toimintaa.

Huomaa

Jotkin Efektit eivät suorita siivousfunktiota ollenkaan. Useimmiten, haluat palauttaa siivousfunktion—mutta jos et, React käyttäytyy kuin olisit palauttanut tyhjän siivousfunktion.

Miksi synkronointi voi tapahtua useammin kuin kerran

Kuvittele, tämä ChatRoom komponetti saa roomId propin, jonka käyttäjä valitsee pudotusvalikosta. Oletetaan, että aluksi käyttäjä valitsee "general" huoneen roomId:ksi. Sovelluksesi näyttää "general" chat-huoneen:

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Kun UI on näytetty, React suorittaa Efektisi aloittaakseen synkronoinnin. Se yhdistää "general" huoneeseen:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Yhdistää "general" huoneeseen
connection.connect();
return () => {
connection.disconnect(); // Katkaisee yhteyden "general" huoneeseen
};
}, [roomId]);
// ...

Tähän asti kaikki hyvin.

Myöhemmin, käyttäjä valitsee eri huoneen pudotusvalikosta (esim. "travel"). Ensin, React päivittää UI:n:

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Ajattele mitä tulisi tapahtua seuraavaksi. Käyttäjä näkee, että "travel" on valittu chat-huoneeksi UI:ssa. Kuitenkin, Efekti joka viimeksi suoritettiin on yhä yhdistetty "general" huoneeseen. roomId propsi on muuttunut, joten mitä Efektisi teki silloin (yhdisti "general" huoneeseen) ei enää vastaa UI:ta.

Tässä vaiheessa, haluat Reactin tekevän kaksi asiaa:

  1. Lopettaa synkronoinnin vanhan roomId kanssa (katkaisee yhteyden "general" huoneeseen)
  2. Aloittaa synkronoinnin uuden roomId kanssa (yhdistää "travel" huoneeseen)

Onneksi, olet jo opettanut Reactille miten teet molemmat näistä asioista! Efektisi runko määrittää miten aloitat synkronoinnin, ja siivousfunktio määrittää miten lopetat synkronoinnin. Kaikki mitä Reactin täytyy tehdä nyt on kutsua niitä oikeassa järjestyksessä ja oikeilla propseilla ja tilalla. Katsotaan mitä oikein tapahtuu.

Miten React uudelleen synkronisoi Efektisi

Muista, että ChatRoom komponenttisi on saanut uuden arvon sen roomId propsiksi. Se olu aluksi "general", ja se on nyt "travel". Reactin täytyy synkronoida Efektisi uudelleen yhdistääkseen sinut eri huoneeseen.

Lopettaaksesi synkronoinnin, React kutsuu siivousfunktiota, jonka Efektisi palautti yhdistettyään "general" huoneeseen. Koska roomId oli "general", siivousfunktio katkaisee yhteyden "general" huoneeseen:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Yhdistää "general" huoneeseen
connection.connect();
return () => {
connection.disconnect(); // Katkaisee yhteyden "general" huoneeseen
};
// ...

React sitten kutsuu Efektiasi, jonka olet tarjonnut tämän renderöinnin aikana. Tällä kertaa, roomId on "travel" joten se aloittaa synkronoinnin "travel" chat-huoneeseen (kunnes sen siivousfunktio kutsutaan):

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Yhdistää "travel" huoneeseen
connection.connect();
// ...

Kiitos tämän, olet nyt yhdistetty samaan huoneeseen, jonka käyttäjä valitsi UI:ssa. Katastrofi vältetty!

Joka kerta kun komponenttisi renderöityy uudelleen eri roomId:llä, Efektisi täytyy synkronoida uudelleen. Esimerkiksi, sanotaan että käyttäjä muuttaa roomId:n arvosta "travel" arvoon "music". Reactin täytyy taas lopettaa synkronointi Efektisi kanssa kutsumalla sen siivousfunktiota (katkaisemalla yhteys "travel" huoneeseen). Sitten se taas aloittaa synkronoinnin suorittamalla Efektisi rungon uudella roomId propsilla (yhdistämällä sinut "music" huoneeseen).

Lopuksi, kun käyttäjä menee eri ruutuun, ChatRoom unmounttaa. Sitten ei ole tarvetta pysyä ollenkaan yhteydessä. React lopettaa synkronoinnin Efektisi kanssa viimeisen kerran ja katkaisee yhteyden "music" huoneeseen.

Ajattelu Efektin perspektiivistä

Käydään läpi kaikki mitä tapahtui ChatRoom komponentin perspektiivissä:

  1. ChatRoom mounttasi roomId arvolla "general"
  2. ChatRoom päivittyi roomId arvolla "travel"
  3. ChatRoom päivittyi roomId arvolla "music"
  4. ChatRoom unmounttasi

Jokaisen kohdan aikana komponentin elinkaaressa, Efektisi teki eri asioita:

  1. Efektisi yhdisti "general" huoneeseen
  2. Efektisi katkaisi yhteyden "general" huoneesta ja yhdisti "travel" huoneeseen
  3. Efektisi katkaisi yhteyden "travel" huoneesta ja yhdisti "music" huoneeseen
  4. Efektisi katkaisi yhteyden "music" huoneesta

Ajatellaan mitä tapahtui Efektin perspektiivistä:

useEffect(() => {
// Efektisi yhdisti huoneeseen, joka määriteltiin roomId:lla...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...kunnes yhteys katkaistiin
connection.disconnect();
};
}, [roomId]);

Tämän koodin rakenne saattaa inspiroida sinua näkemään mitä tapahtui sekvenssinä ei-päällekkäisistä aikajaksoista:

  1. Efektisi yhdisti "general" huoneeseen (kunnes yhteys katkaistiin)
  2. Efektisi yhdisti "travel" huoneeseen (kunnes yhteys katkaistiin)
  3. Efektisi yhdisti "music" huoneeseen (kunnes yhteys katkaistiin)

Aiemmin, ajattelit komponentin perspektiivistä. Kun katsot sitä komponentin perspektiivistä, oli houkuttelevaa ajatella Efektejä “callbackeina” tai “elinkaari-tapahtumina”, jotka tapahtuvat tiettyyn aikaan kuten “renderöinnin jälkeen” tai “ennen unmounttaamista”. Tämä ajattelutapa monimutkaistuu nopeasti, joten on parempi välttää sitä.

Sen sijaan, keskity aina yksittäiseen alku/loppu sykliin kerralla. Sillä ei tulisi olla merkitystä mounttaako, päivittyykö, vai unmounttaako komponentti. Sinun täytyy vain kuvailla miten aloitat synkronoinnin ja miten lopetat sen. Jos teet sen hyvin, Efektisi on kestävä aloittamiselle ja lopettamiselle niin monta kertaa kuin tarpeellista.

Tämä saattaa muistuttaa sinua siitä, miten et ajattele mounttaako vai päivittyykö komponentti kun kirjoitat renderöintilogiikkaa, joka luo JSX:ää. Kuvailet mitä pitäisi olla näytöllä, ja React selvittää loput.

Miten React vahvistaa, että Efektisi voi synkronoitua uudelleen

Tässä on esimerkki, jota voit kokeilla. Paina “Open chat” mountataksesi ChatRoom komponentin:

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');
  const [show, setShow] = useState(false);
  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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Huomaa, että kun komponentti mounttaa ensimmäisen kerran, näet kolme lokia:

  1. ✅ Connecting to "general" room at https://localhost:1234... (vain-kehitysvaiheessa)
  2. ❌ Disconnected from "general" room at https://localhost:1234. (vain-kehitysvaiheessa)
  3. ✅ Connecting to "general" room at https://localhost:1234...

Ensimmäiset kaksi ovat vain kehityksessä. Kehityksessä, React uudelleen mounttaa jokaisen komponentin kerran.

React vahvistaa, että Efektisi voi synkronoitua uudelleen pakottamalla sen tekemään se välittömästi kehityksessä. Tämä saattaa muistuttaa sinua oven avaamisesta ja sulkemisesta ylimääräisen kerran tarkistaaksesi, että lukko toimii. React aloittaa ja lopettaa Efektisi yhden ylimääräisen kerran kehityksessä tarkistaakseen, että olet toteuttanut siivousfunktion hyvin.

Pääsyy miksi Efektisi synkronoi uudelleen käytännössä on jos jokin data, jota se käyttää on muuttunut. Yllä olevassa hiekkalaatikossa, vaihda valittua chat-huonetta. Huomaa miten Efektisi synkronoituu uudelleen roomId muuttuessa.

Kuitenkin, on myös epätavallisempia tapauksia, joissa uudelleen synkronointi on tarpeellista. Esimerkiksi, kokeile muokata serverUrl:ää hiekkalaatikossa yllä kun chat on auki. Huomaa miten Efektisi synkronoituu uudelleen vastauksena koodin muokkaukseen. Tulevaisuudessa, React saattaa lisätä lisää ominaisuuksia, jotka nojaavat uudelleen synkronointiin.

Miten React tietää, että sen täytyy synkronoida Efekti uudelleen

Saatat miettiä miten React tiesi, että Efektisi täytyi synkronoida uudelleen roomId:n muuttuessa. Se johtuu siitä, että kerroit Reactille koodin riippuvan roomId:sta sisällyttämällä sen riippuvuustaulukkoon:

function ChatRoom({ roomId }) { // roomId propsi saattaa muuttua ajan kanssa
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Tämä Efekti lukee roomId:n
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // Joten kerrot Reactille, että tämä Efekti "riippuu" roomId:sta
// ...

Tässä miten tämä toimii:

  1. You knew roomId is a prop, which means it can change over time.
  2. Tiesit roomId:n olevan propsi, joka tarkoittaa, että se voi muuttua ajan kanssa.
  3. Tiesit, että Efektisi lukee roomId:n (joten sen logiikka riippuu arvosta, joka saattaa muuttua myöhemmin).
  4. Tämä on miksi määritit sen Efektisi riippuvuudeksi (jotta se synkronoituu uudelleen kun roomId muuttuu).

Joka kerta kun komponenttisi renderöityy uudelleen, React katsoo riippuvuustaulukkoa, jonka olet määrittänyt. Jos mikään arvoista taulukossa on eri kuin arvo samassa kohdassa, jonka annoit edellisellä renderöinnillä, React synkronoi Efektisi uudelleen.

Esimerkiksi, jos välitit arvon ["general"] ensimmäisen renderöinnin aikana, ja myöhemmin välitit ["travel"] seuraavan renderöinnin aikana, React vertaa "general" ja "travel" arvoja. Nämä ovat eri arvoja (vertailtu Object.is avulla), joten React synkronoi Efektisi uudelleen. Toisaalta, jos komponenttisi uudelleen renderöityy mutta roomId ei ole muuttunut, Efektisi pysyy yhdistettynä samaan huoneeseen.

Kukin Efekti edustaa erillistä synkronointiprosessia

Vältä lisäämästä aiheesta poikkeavaa logiikkaa Efektiisi vain koska tämä logiikka täytyy suorittaa samaan aikaan kuin Efekti, jonka olet jo kirjoittanut. Esimerkiksi, oletetaan että haluat lähettää analytiikka tapahtuman kun käyttäjä vierailee huoneessa. Sinulla on jo Efekti, joka riippuu roomId:sta, joten saatat tuntea houkutuksen lisätä analytiikka-kutsu sinne:

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

Mutta kuvittele, että myöhemmin lisäät toisen riippuvuuden tähän Efektiin, joka täytyy alustaa yhteys uudelleen. Jos tämä Efekti synkronoituu uudelleen, se kutsuu myös logVisit(roomId) samaan huoneeseen, jota et tarkoittanut. Vierailun kirjaaminen on erillinen prosessi yhdistämisestä. Kirjoita ne kahdeksi erilliseksi Efektiksi:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

Jokaisen Efektin koodissasi tulisi edustaa erillistä ja riippumatonta synkronointiprosessia.

Yllä olevassa esimerkissä, yhden Efektin poistaminen ei hajota toisen Efektin logiikkaa. Tämä on hyvä indikaatio siitä, että ne synkronoivat eri asioita, joten oli järkevää jakaa ne kahteen erilliseen Efektiin. Toisaalta, jos jaat yhtenäisen logiikan eri Efekteihin, koodi saattaa näyttää “puhtaammalta” mutta tulee vaikeammaksi ylläpitää. Tämän takia sinun tulisi ajatella ovatko prosessit samat vai erilliset, eivät sitä näyttääkö koodi puhtaammalta.

Efektit “reagoivat” reaktiivisiin arvoihin

Efektisi lukee kaksi muuttujaa (serverUrl ja roomId), mutta määritit vain roomId:n riippuvuudeksi:

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

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

Miksei serverUrl tarvitse olla riippuvuus?

Tämä siksi, koska serverUrl ei muutu koskaan uudelleen renderöinnin seurauksena. Se on aina sama riippumatta kuinka monta kertaa komponentti renderöityy ja miksi. Koska serverUrl ei koskaan muutu, ei olisi järkevää määrittää sitä riippuvuudeksi. Loppujen lopuksi, riippuvuudet tekevät jotain vain kun ne muuttuvat ajan kanssa!

Toisella kädellä, roomId saattaa olla eri uudelleen renderöinnin seurauksena. Propsit, tila, ja muut arvot, jotka on määritelty komponentin sisällä ovat reaktiivisia koska ne lasketaan renderöinnin aikana ja osallistuvat Reactin datavirtaan.

Jos serverUrl olisi tilamuuttuja, se olisi reaktiivinen. Reaktiiviset arvot täytyy sisällyttää riippuvuuksiin:

function ChatRoom({ roomId }) { // Propsit muuttuvat ajan kanssa
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // Tila voi muuttua ajan kanssa

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Efektisi lukee propsin ja tilan
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // Joten kerrot Reactille, että tämä Efekti "riippuu" propsista ja tilasta
// ...
}

Sisällyttämällä serverUrl riippuvuudeksi, varmistat että Efekti synkronoituu uudelleen sen muuttuessa.

Kokeile muuttaa valittua chat-huonetta tai muokata server URL:ää tässä hiekkalaatikossa:

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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} />
    </>
  );
}

Aina kun muutat reaktiivista arvoa kuten roomId tai serverUrl, Efekti yhdistää uudelleen chat-palvelimeen.

Mitä tyhjä riippuvuustaulukko tarkoittaa

Mitä tapahtuu jos siirrät molemmat serverUrl ja roomId komponentin ulkopuolelle?

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

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

Nyt Efektisi koodi ei käytä yhtään reaktiivista arvoa, joten sen riippuvuustaulukko voi olla tyhjä ([]).

Ajattelu komponentin perspektiivista, tyhjä [] riippuvuustaulukko tarkoittaa, että tämä Efekti yhdistää chat-huoneeseen vain kun komponentti mounttaa, ja katkaisee yhteyden vain kun komponentti unmounttaa. (Pidä mielessä, että React silti synkronoi ylimääräisen kerran kehityksessä testatakseen logiikkaasi.)

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

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

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

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

Kuitenkin, jos ajattelet Efektin perspektiivista, sinun ei tarvitse ajatella mountaamista ja unmountaamista ollenkaan. Tärkeää on se, että olet määritellyt mitä Efektisi tarvitsee synkronoinnin aloittamiseen ja lopettamiseen. Tänään, sillä ei ole yhtään reaktiivista riippuvuutta. Mutta jos haluat koskaan käyttäjän muuttavan roomId:n tai serverUrl:n ajan kanssa (ja ne tulisivat reaktiivisiksi), Efektisi koodi ei muutu. Sinun täytyy vain lisätä ne riippuvuuksiin.

Kaikki muuttujat komponentin sisällä ovat reaktiivisia

Propsit ja tila eivät ole ainoita reaktiivisia arvoja. Arvot jotka niistä lasket ovat myös reaktiivisia. Jos propsit tai tila muuttuu, komponenttisi tulee renderöitymään uudelleen, ja niistä lasketut arvot myös muuttuvat. Tämä on syy miksi kaikki Efektin tulisi lisätä Efektin riippuvuustaulukkoon kaikki komponentin sisällä olevat muuttujat, joita se käyttää.

Sanotaan, että käyttäjä voi valita chat-palvelimen pudotusvalikosta, mutta he voivat myös määrittää oletuspalvelimen asetuksissa. Oletetaan, että olet jo laittanut asetukset -tilan kontekstiin, joten luet settings:in siitä kontekstista. Nyt lasket serverUrl:n valitun palvelimen ja oletuspalvelimen perusteella:

function ChatRoom({ roomId, selectedServerUrl }) { // roomId on reaktiivinen
const settings = useContext(SettingsContext); // settings on reaktiivinen
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl on reaktiivinen
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Efektisi lukee roomId ja serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // Joten sen täytyy synkronoida uudelleen kun jompi kumpi niistä muuttuu!
// ...
}

Tässä esimerkissä, serverUrl ei ole propsi tai tilamuuttuja. Se on tavallinen muuttuja, jonka lasket renderöinnin aikana. Mutta se on laskettu renderöinnin aikana, jolloin se voi muuttua uudelleen renderöinnin seurauksena. Tämä on syy miksi se on reaktiivinen.

Kaikki komponentin sisällä olevat arvot (mukaan lukien propsit, tila, ja muuttujat komponenttisi sisällä) ovat reaktiivisia. Mikä tahansa reaktiivinen arvo voi muuttua uudelleen renderöinnin seurauksena, joten sinun täytyy sisällyttää reaktiiviset arvot Efektin riippuvuustaulukkoon.

Toisin sanoen, Efektisi “reagoi” kaikkii arvoihin komponentin sisällä.

Syväsukellus

Voiko globaalit tai mutatoitavat arvot olla riippuvuuksia?

Mutatoitavat arovot (mukaan lukien globaalit muttujat) eivät ole reaktiivisia.

Mutatoitava arvo kuten location.pathname ei voi olla riippuvuus. Se on mutatoitavissa, joten se voi muuttua koska vain täysin Reactin renderöinnin datavirtauksen ulkopuolella. Sen muuttaminen ei käynnistäisi komponenttisi uudelleenrenderöintiä. Tämän takia, vaikka määrittelisit sen riippuvuuksissasi, React ei tietäisi synkronoida Efektiasi uudelleen sen muuttuessa. Tämä myös rikkoo Reactin sääntöjä, koska mutatoitavan datan lukeminen renderöinnin aikana (joka on kun lasket riippuvuuksia) rikkoo renderöinnin puhtauden. Sen sijaan, sinun tulisi lukea ja tilata ulkoisesta mutatoitavasta arvosta useSyncExternalStore:n avulla.

Mutatoitava arvo kuten ref.current tai siitä luettavat asiat eivät myöskään voi olla riippuvuuksia. useRef:n palauttama ref-objekti voi olla riippuvuus, mutta sen current-ominaisuus on tarkoituksella mutatoitava. Sen avulla voit pitää kirjaa jostain ilman, että se käynnistää uudelleenrenderöinnin. Mutta koska sen muuttaminen ei käynnistä uudelleenrenderöintiä, se ei ole reaktiivinen arvo, eikä React tiedä synkronoida Efektiasi uudelleen sen muuttuessa.

Kuten tulet oppimaan tällä sivulla, linter tulee myös tarkistamaan näitä ongelmia automaattisesti.

React tarkistaa, että olet määrittänyt jokaisen reaktiivisen arvon riippuvuudeksi

Jos linterisi on konfiguroitu Reactille, se tarkistaa, että jokainen reaktiivinen arvo, jota Efektisi koodi käyttää on määritelty sen riippuvuudeksi. Esimerkiksi, tämä on linterin virhe koska molemmat roomId ja serverUrl ovat reaktiivisia:

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

function ChatRoom({ roomId }) { // roomId on reaktiivinen
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl on reaktiivinen

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Jokin on pielessä täällä!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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ämä saattaa näyttää React-virheeltä, mutta oikeasti React näyttää bugin koodissasi. Molemmat roomId sekä serverUrl voivat muuttua ajan kanssa, mutta unohdat synkronoida Efektisi kun ne muuttuvat. Pysyt yhdistettynä alkuperäiseen roomId ja serverUrl vaikka käyttäjä valitsisi eri arvot käyttöliittymässä.

Korjataksesi bugin, seuraa linterin ehdotusta määrittääksesi roomId ja serverUrl Efektisi riippuvuuksiksi:

function ChatRoom({ roomId }) { // roomId on reaktiivinen
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl on reaktiivinen
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ Kaikki riippuvuudet määritelty
// ...
}

Kokeile tätä korjausta hiekkalaatikossa yllä. Varmista, että linterin virhe on poistunut, ja chat yhdistyy kun se on tarpeellista.

Huomaa

Jossain tapauksissa, React tietää, että arvo ei tule koskaan muuttumaan vaikka se on määritelty komponentin sisällä. Esimerkiksi, set-funktio joka palautetaan useState:sta ja ref-objekti, joka palautetaan useRef ovat stabiileja—niiden on taattu olevan muuttumattomia uudelleen renderöinnin seurauksena. Stabiilit arvot eivät ole reaktiivisia, joten voit jättää ne pois listasta. Niiden sisällyttäminen on sallittua: ne eivät muutu, joten sillä ei ole väliä.

Mitä tehdä kun et halua synkronoida uudelleen

Edellisessä esimerkissä, olet korjannut linter-virheen listaamalla roomId ja serverUrl riippuvuuksina.

Kuitenkin, voisit sen sijaan “todistaa” linterille, että nämä arvot eivät ole reaktiivisia arvoja, eli että ne eivät voi muuttua uudelleen renderöinnin seurauksena. Esimerkiksi, jos serverUrl ja roomId eivät riipu renderöinnistä ja ovat aina samoja arvoja, voit siirtää ne komponentin ulkopuolelle. Nyt niiden ei tarvitse olla riippuvuuksia:

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

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

You can also move them inside the Effect. They aren’t calculated during rendering, so they’re not reactive:

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effects are reactive blocks of code. They re-synchronize when the values you read inside of them change. Unlike event handlers, which only run once per interaction, Effects run whenever synchronization is necessary.

You can’t “choose” your dependencies. Your dependencies must include every reactive value you read in the Effect. The linter enforces this. Sometimes this may lead to problems like infinite loops and to your Effect re-synchronizing too often. Don’t fix these problems by suppressing the linter! Here’s what to try instead:

  • Check that your Effect represents an independent synchronization process. If your Effect doesn’t synchronize anything, it might be unnecessary. If it synchronizes several independent things, split it up.

  • If you want to read the latest value of props or state without “reacting” to it and re-synchronizing the Effect, you can split your Effect into a reactive part (which you’ll keep in the Effect) and a non-reactive part (which you’ll extract into something called an Effect Event). Read about separating Events from Effects.

  • Avoid relying on objects and functions as dependencies. If you create objects and functions during rendering and then read them from an Effect, they will be different on every render. This will cause your Effect to re-synchronize every time. Read more about removing unnecessary dependencies from Effects.

Sudenkuoppa

The linter is your friend, but its powers are limited. The linter only knows when the dependencies are wrong. It doesn’t know the best way to solve each case. If the linter suggests a dependency, but adding it causes a loop, it doesn’t mean the linter should be ignored. You need to change the code inside (or outside) the Effect so that that value isn’t reactive and doesn’t need to be a dependency.

If you have an existing codebase, you might have some Effects that suppress the linter like this:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

On the next pages, you’ll learn how to fix this code without breaking the rules. It’s always worth fixing!

Kertaus

  • Components can mount, update, and unmount.
  • Each Effect has a separate lifecycle from the surrounding component.
  • Each Effect describes a separate synchronization process that can start and stop.
  • When you write and read Effects, think from each individual Effect’s perspective (how to start and stop synchronization) rather than from the component’s perspective (how it mounts, updates, or unmounts).
  • Values declared inside the component body are “reactive”.
  • Reactive values should re-synchronize the Effect because they can change over time.
  • The linter verifies that all reactive values used inside the Effect are specified as dependencies.
  • All errors flagged by the linter are legitimate. There’s always a way to fix the code to not break the rules.

Haaste 1 / 5:
Fix reconnecting on every keystroke

In this example, the ChatRoom component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working.

However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesn’t happen.

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

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

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

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

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