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 propseihin 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. Tiesit roomId:n olevan propsi, joka tarkoittaa, että se voi muuttua ajan kanssa.
  2. Tiesit, että Efektisi lukee roomId:n (joten sen logiikka riippuu arvosta, joka saattaa muuttua myöhemmin).
  3. 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 ei ole reaktiivinen
const roomId = 'general'; // roomId ei ole reaktiivinen

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

Voit myös siirtää ne Efektin sisälle. Niitä ei lasketa renderöinnin aikana, joten ne eivät ole reaktiivisia:

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

Efektit ovat reaktiivisia koodinpalasia. Ne synkronoituvat uudelleen kun arvot, joita niiden sisällä luet muuttuvat. Toisin kuin tapahtumankäsittelijät, jotka suoritetaan vain kerran jokaista interaktiota kohden, Efektit suoritetaan aina kun synkronointi on tarpeellista.

Et voi “päättää” riippuvuuksiasi. Riippuvuutesi täytyy sisältää kaikki reaktiiviset arvot, joita luet Efektissa. Linter edellyttää tätä. Joskus tämä saattaa aiheuttaa ongelmia kuten loputtomia silmukoita ja Efektisi liian usein synkronoimisen. Älä korjaa näitä ongelmia hiljentämällä linter! Kokeile sen sijaan seuraavaa:

  • Tarkista, että Efektisi edustaa yksittäistä synkronointiprosessia. Jos Efektisi ei synkronoi mitään, se saattaa olla turha. Jos se synkronoi useita yksittäisiä asioita, jaa se osiin.

  • Jos haluat lukea propsien tai tilan arvon ilman “reagointia” tai Efektin synkronoimista uudelleen, voit jakaa Efektisi reaktiiviseen osaan (joka pidetään Efektissä) ja ei-reaktiiviseen osaan (joka erotetaan Efektin tapahtumaksi). Lue lisää tapahtumien erottamisesta Efekteistä.

  • Vältä riippuvuutta olioista ja funktioista. Jos luot olioita ja funktioita renderöinnin aikana ja luet niitä Efekteissä, ne ovat uusia joka renderöinnillä. Tämä aiheuttaa Efektisi synkronoimisen uudelleen joka kerta. Lue lisää tarpeettomien riippuvuuksien poistamisesta Efekteistä.

Sudenkuoppa

Linter on ystäväsi, mutta sen voimat ovat rajattuja. Linter tietää vain koska riippuvuutesi on väärin. Se ei tiedä parasta tapaa ratkaista jokaista tilannetta. Jos linter suosittelee riippuvuutta, mutta sen lisääminen aiheuttaa silmukan, se ei tarkoita, että linter tulisi sivuuttaa. Sinun täytyy muuttaa koodia Efektin sisällä (tai ulkopuolella) niin, että arvo ei ole reaktiivinen eikä tarvitse olla riippuvuus.

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

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

Seuraavalla sivulla, opit korjaamaan tämän koodin rikkomatta sääntöjä. Se on aina korjaamisen arvoista!

Kertaus

  • Komponentit voivat mountata, päivittää, ja unmountata.
  • Jokaisella Efektilla on erillinen elinkaari ympäröivästä komponentista.
  • Jokainen Efekti kuvaa erillistä synkronointiprosessia, joka voi alkaa ja loppua.
  • Kun kirjoitat ja luet Efekteja, ajattele jokaisen yksittäisen Efektin perspektiivistä (miten aloittaa ja lopettaa synkronointi) sen sijaan, että ajattelisit komponentin perspektiivistä (miten se mounttaa, päivittyy, tai unmounttaa).
  • Komponentin sisällä määritellyt arvot ovat “reaktiivisia”.
  • Reaktiivisten arvojen tulisi synkronoida Efekti uudelleen koska ne voivat muuttua ajan kanssa.
  • Linter vahvistaa, että kaikki Efektin sisällä käytetyt reaktiiviset arvot on määritelty riippuvuuksiksi.
  • Kaikki linterin virheet ovat todellisia. On aina tapa korjata koodi siten, ettei se riko sääntöjä.

Haaste 1 / 5:
Korjaa yhdistäminen jokaisella näppäinpainalluksella

Tässä esimerkissä, ChatRoom komponentti yhdistää chat-huoneeseen kun komponentti mounttaa, katkaisee yhteyden kun se unmounttaa, ja yhdistää uudelleen kun valitset eri chat-huoneen. Tämä käyttäytyminen on oikein, joten sinun täytyy pitää se toiminnassa.

Kuitenkin, on ongelma. Aina kun kirjoitat viestilaatikkoon alhaalla, ChatRoom myös yhdistää chatiin uudelleen. (Voit huomata tämän tyhjentämällä konsolin ja kirjoittamalla viestilaatikkoon.) Korjaa ongelma niin, ettei näin tapahdu.

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