Synkronointi Effecteilla

Joidenkin komponenttien täytyy synkronoida ulkoisten järjestelmien kanssa. Esimerkiksi saatat haluta hallita ei-React-komponenttia perustuen Reactin tilaan, asettaa palvelinyhteyden tai lähettää analytiikkalokeja, kun komponentti näkyy näytöllä. Effectit mahdollistavat koodin suorittamisen renderöinnin jälkeen, jotta voit synkronoida komponentin jonkin ulkoisen järjestelmän kanssa Reactin ulkopuolella.

Tulet oppimaan

  • Mitä Effectit ovat
  • Miten Effectit eroavat tapahtumista
  • Miten määrittelet Effecti komponentissasi
  • Miten ohitat Effectin tarpeettoman suorittamisen
  • Miksi Effectit suoritetetaan kahdesti kehitysympäristössä ja miten sen voi korjata

Mitä Effectit ovat ja miten ne eroavat tapahtumista?

Ennen kuin siirrytään Effecteihin, tutustutaan kahdenlaiseen logiikkaan React-komponenteissa:

  • Renderöintikoodi (esitellään Käyttöliittymän kuvauksessa) elää komponentin yläpuolella. Tässä on paikka missä otat propsit ja tilan, muunnet niitä ja palautat JSX:ää, jonka haluat nähdä näytöllä. Renderöintikoodin on oltava puhdasta. Kuten matemaattinen kaava, sen tulisi vain laskea tulos, mutta ei tehdä mitään muuta.

  • Tapahtumakäsittelijät (esitellään Interaktiivisuuden lisäämisessä) ovat komponenttien sisäisiä funktioita, jotka tekevät asioita sen sijaan, että vain laskisivat asioita. Tapahtumakäsittelijä saattavat päivittää syöttökenttää, lähettää HTTP POST -pyyntöjä ostaakseen tuoteen tai ohjata käyttäjän toiselle näytölle. Tapahtumakäsittelijät sisältävät “sivuvaikutuksia” (ne muuttavat ohjelman tilaa) ja aiheutuvat tietystä käyttäjän toiminnasta (esimerkiksi painikkeen napsauttamisesta tai kirjoittamisesta).

Joskus tämä ei riitä. Harkitse ChatRoom -komponenttia, jonka täytyy yhdistää keskustelupalvelimeen, kun se näkyy näytöllä. Palvelimeen yhdistäminen ei ole puhdas laskenta (se on sivuvaikutus), joten se ei voi tapahtua renderöinnin aikana. Kuitenkaan ei ole yhtä tiettyä tapahtumaa, kuten napsautusta, joka aiheuttaisi ChatRoom -komponentin näkymisen.

Effectien avulla voit määritellä sivuvaikutukset, jotka johtuvat renderöinnistä itsestään, eikä tietystä tapahtumasta. Viestin lähettäminen keskustelussa on tapahtuma, koska se aiheutuu suoraan käyttäjän napsauttamasta tiettyä painiketta. Kuitenkin palvelimen yhdistäminen on effect, koska se on tehtävä riippumatta siitä, mikä vuorovaikutus aiheutti komponentin näkyvyyden. Effectit suoritetaan renderöintiprosessin lopussa näytön päivityksen jälkeen. Tässä on hyvä aika synkronoida React-komponentit jonkin ulkoisen järjestelmän kanssa (kuten verkon tai kolmannen osapuolen kirjaston).

Huomaa

Tässä ja myöhemmin tekstissä, “Effect”:llä viittaamme Reactin määritelmään, eli sivuvaikutukseen, joka aiheutuu renderöinnistä. Viittaaksemme laajempaan ohjelmointikäsitteeseen, sanomme “sivuvaikutus”.

Et välttämättä tarvitse Effectia

Älä kiiruhda lisäämään Effecteja komponentteihisi. Pidä mielessä, että Effectit ovat tyypillisesti tapa “astua ulos” React-koodistasi ja synkronoida jonkin ulkoisen järjestelmän kanssa. Tämä sisältää selaimen API:t, kolmannen osapuolen pienoisohjelmat, verkon jne. Jos Effectisi vain muuttaa tilaa perustuen toiseen tilaan, voit ehkä jättää Effectin pois.

Miten kirjoitat Effectin

Kirjoittaaksesi Effectin, seuraa näitä kolmea vaihetta:

  1. Määrittele Effect. Oletuksena, Effectisi suoritetaan jokaisen renderöinnin jälkeen.
  2. Määrittele Effectin riippuvuudet. Useimmat Effectit pitäisi suorittaa vain tarvittaessa sen sijaan, että ne suoritettaisiin jokaisen renderöinnin jälkeen. Esimerkiksi fade-in -animaatio pitäisi käynnistyä vain, kun komponentti ilmestyy. Keskusteluhuoneeseen yhdistäminen ja sen katkaisu pitäisi tapahtua vain, kun komponentti ilmestyy ja häviää tai kun keskusteluhuone muuttuu. Opit hallitsemaan tätä määrittämällä riippuvuudet.
  3. Lisää puhdistus, jos tarpeen. Joidenkin Effectien täytyy määrittää, miten ne pysäytetään, peruutetaan, tai puhdistavat mitä ne ovat tehneet. Esimerkiksi “yhdistys” tarvitsee “katkaisun”, “tila” tarvitsee “peruuta tilaus” ja “hae” tarvitsee joko “peruuta” tai “jätä huomiotta”. Opit tekemään tämän palauttamalla puhdistusfunktion.

Katsotaan näitä vaiheita yksityiskohtaisesti.

1. Vaihe: Määrittele Effect

Määritelläksesi Effectin komponentissasi, tuo useEffect Hook Reactista:

import { useEffect } from 'react';

Sitten kutsu sitä komponentin yläpuolella ja laita koodia Effectin sisään:

function MyComponent() {
useEffect(() => {
// Koodi täällä suoritetaan *jokaisen* renderöinnin jälkeen
});
return <div />;
}

Joka kerta kun komponenttisi renderöityy, React päivittää ruudun ja sitten suorittaa koodin useEffect:n sisällä. Toisin sanoen, useEffect “viivästää” koodin suorittamista, kunnes renderöinti on näkyvissä ruudulla.

Katsotaan miten voit käyttää Effectia synkronoidaksesi ulkoisen järjestelmän kanssa. Harkitse <VideoPlayer> React komponenttia. Olisi mukavaa kontrolloida, onko video toistossa vai pysäytettynä, välittämällä isPlaying propsin sille:

<VideoPlayer isPlaying={isPlaying} />;

Sinun mukautettu VideoPlayer komponentti renderöi selaimen sisäänrakennetun <video> tagin:

function VideoPlayer({ src, isPlaying }) {
// TODO: tee jotain isPlaying:lla
return <video src={src} />;
}

Kuitenkaan selaimen <video> tagissa ei ole isPlaying proppia. Ainoa tapa ohjata sitä on manuaalisesti kutsua play() ja pause() metodeja DOM elementillä. Sinun täytyy synkronoida isPlaying propin arvo, joka kertoo, pitäisikö video nyt toistaa, imperatiivisilla kutsuilla kuten play() ja pause().

Meidän täytyy ensiksi hakea ref <video>:n DOM noodiin.

Saattaa olla houkuttelevaa kutsua play() tai pause() metodeja renderöinnin aikana, mutta se ei ole oikein:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  if (isPlaying) {
    ref.current.play();  // Tämän kutsuminen renderöinnin aikana ei ole sallittua.
  } else {
    ref.current.pause(); // Tämä myöskin kaatuu.
  }

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Syy miksi tämä koodi ei ole oikein on, että se koittaa tehdä jotain DOM noodilla kesken renderöinnin. Reactissa renderöinnin tulisi olla puhdas laskelma JSX:stä ja sen ei tulisi sisältää sivuvaikutuksia kuten DOM:n muuttamista.

Lisäksi, kun VideoPlayer kutsutaan ensimmäistä kertaa, sen DOM ei vielä ole olemassa! Ei ole vielä DOM noodia josta kutsua play() tai pause() koska React ei tiedä mitä DOM:ia luoda ennen kuin palautat JSX:n.

Ratkaisu tässä on kääriä sivuvaikutus useEffectilla ja siirtää se pois renderöintilaskusta:

import { useEffect, useRef } from 'react';

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);

useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});

return <video ref={ref} src={src} loop playsInline />;
}

Käärimällä DOM päivitys Effectiin, annat Reactin päivittää ensin ruudun. Sitten Effectisi suoritetaan.

Kun VideoPlayer komponenttisi renderöityy (joko ensimmäistä kertaa tai jos se renderöityy uudelleen), tapahtuu muutamia asioita. Ensimmäiseksi React päivittää ruudun, varmistaen että <video> tagi on DOM:issa oikeilla propseilla. Sitten React suorittaa Effectisi. Lopuksi, Effectisi kutsuu play() tai pause() riippuen isPlaying propin arvosta.

Paina Play/Pause useita kertoja ja katso miten videoplayer pysyy synkronoituna isPlaying arvon kanssa:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Tässä esimerkissä “ulkoinen järjestelmä” jonka kanssa synkronoit Reactin tilan oli selaimen media API. Voit käyttää samanlaista lähestymistapaa kääriäksesi legacy ei-React koodin (kuten jQuery pluginit) deklaratiivisiin React komponentteihin.

Huomaa, että videoplayerin ohjaaminen on paljon monimutkaisempaa käytännössä. play() kutsu voi epäonnistua, käyttäjä voi toistaa tai pysäyttää videon käyttämällä selaimen sisäänrakennettuja ohjauselementtejä, jne. Tämä esimerkki on hyvin yksinkertaistettu ja puutteellinen.

Sudenkuoppa

Oletuksena Effectit suoritetaan jokaisen renderöinnin jälkeen. Tämä on syy miksi seuraavanlainen koodi tuottaa loputtoman silmukan:

const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});

Effectit suoritetaan renderöinnin johdosta. Tilan asettaminen aiheuttaa renderöinnin. Tilan asettaminen välittömästi Effectissä on kuin pistäisi jatkojohdon kiinni itseensä. Effect suoritetaan, se asettaa tilan, joka aiheuttaa uudelleen renderöinnin, joka aiheuttaa Effectin suorittamisen, joka asettaa tilan uudelleen, joka aiheuttaa uudelleen renderöinnin, ja niin edelleen.

Effectien tulisi yleensä synkronoida komponenttisi ulkopuolisen järjestelmän kanssa. Jos ei ole ulkopuolista järjestelmää ja haluat vain muuttaa tilaa perustuen toiseen tilaan, voit ehkä jättää Effectin pois.

2. Vaihe: Määrittele Effectin riippuvuudet

Oletuksena Effectit toistetaan jokaisen renderöinnin jälkeen. Usein tämä ei ole mitä haluat:

  • Joskus, se on hidas. Synkronointi ulkoisen järjestelmän kanssa ei aina ole nopeaa, joten haluat ehkä ohittaa sen, ellei sitä ole tarpeen. Esimerkiksi, et halua yhdistää chat palvelimeen jokaisen näppäinpainalluksen jälkeen.
  • Joksus, se on väärin. Esimerkiksi, et halua käynnistää komponentin fade-in animaatiota jokaisen näppäinpainalluksen jälkeen. Animaation pitäisi toistua pelkästään kerran kun komponentti ilmestyy ensimmäisellä kerralla.

Havainnollistaaksemme ongelmaa, tässä on edellinen esimerkki muutamalla console.log kutsulla ja tekstikentällä, joka päivittää vanhemman komponentin tilaa. Huomaa miten kirjoittaminen aiheuttaa Effectin uudelleen suorittamisen:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Voit kertoa Reactin ohittamaan tarpeettoman Effectin uudelleen suorittamisen määrittelemällä riippuvuus taulukon toisena argumenttina useEffect kutsulle. Aloita lisäämällä tyhjä [] taulukko ylläolevaan esimerkkiin riville 14:

useEffect(() => {
// ...
}, []);

Sinun tulisi nähdä virhe, jossa lukee React Hook useEffect has a missing dependency: 'isPlaying':

useEffect(() => {
// ...
}, []);
import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  }, []); // Tämä aiheuttaa virheen

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Ongelma on, että effectin sisällä oleva koodi riippuu isPlaying propsin arvosta päättääkseen mitä tehdä, mutta tätä riippuvuutta ei ole määritelty. Korjataksesi tämän ongelman, lisää isPlaying riippuvuustaulukkoon:

useEffect(() => {
if (isPlaying) { // Sitä käytetään tässä...
// ...
} else {
// ...
}
}, [isPlaying]); // ...joten se täytyy määritellä täällä!

Nyt kaikki riippuvuudet on määritelty, joten virheitä ei ole. [isPlaying] riippuvuustaulukon määrittäminen kertoo Reactille, että se pitäisi ohittaa Effectin uudelleen suorittaminen jos isPlaying on sama kuin se oli edellisellä renderöinnillä. Tämän muutoksen jälkeen, tekstikenttään kirjoittaminen ei aiheuta Effectin uudelleen suorittamista, mutta Play/Pause painikkeen painaminen aiheuttaa:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Calling video.play()');
      ref.current.play();
    } else {
      console.log('Calling video.pause()');
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Riippuvuustaulukko voi sisältää useita riippuvuuksia. React ohittaa Effectin uudelleen suorittamisen vain jos kaikki riippuvuudet ovat samat kuin edellisellä renderöinnillä. React vertaa riippuvuuksien arvoja käyttäen Object.is vertailua. Katso useEffect API viittaus lisätietoja varten.

Huomaa, että et voi “valita” riippuvuuksiasi. Jos määrittelemäsi riippuvuudet eivät vastaa Reactin odottamia riippuvuuksia, saat linter virheen. Tämä auttaa löytämään useita virheitä koodissasi. Jos Effect käyttää jotain arvoa, mutta et halua suorittaa Effectiä uudelleen kun se muuttuu, sinun täytyy muokata Effectin koodia itse jotta se ei “tarvitse” tätä riippuvuutta.

Sudenkuoppa

Käyttäytyminen ilman riippuvuustaulukkoa ja tyhjällä [] riippuvuustaulukolla ovat hyvin erilaisia:

useEffect(() => {
// Tämä suoritetaan joka kerta kun komponentti renderöidään
});

useEffect(() => {
// Tämä suoritetaan vain mountattaessa (kun komponentti ilmestyy)
}, []);

useEffect(() => {
// Tämä suoritetaan mountattaessa *ja myös* jos a tai b ovat
// muuttuneet viime renderöinnin jälkeen
}, [a, b]);

Katsomme seuraavassa vaiheessa tarkemmin mitä “mount” tarkoittaa.

Syväsukellus

Miksi ref oli jätetty riippuvuustaulukosta pois?

Tämä Effecti käyttää sekä ref että isPlaying:ä, mutta vain isPlaying on määritelty riippuvuustaulukkoon:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);

Tämä tapahtuu koska ref oliolla on vakaa identiteetti: React takaa että saat aina saman olion samasta useRef kutsusta joka renderöinnillä. Se ei koskaan muutu, joten se ei koskaan itsessään aiheuta Effectin uudelleen suorittamista. Siksi ei ole merkityksellistä onko se määritelty riippuvuustaulukkoon vai ei. Sen sisällyttäminen on myös ok:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);

useState:n palauttamilla set funktioilla on myös vakaa identiteetti, joten näet usein että se jätetään riippuvuustaulukosta pois. Jos linter sallii riippuvuuden jättämisen pois ilman virheitä, se on turvallista tehdä.

Aina-vakaiden riippuvuuksien jättäminen pois toimii vain kun linter voi “nähdä”, että olio on vakaa. Esimerkiksi, jos ref välitetään yläkomponentilta, sinun täytyy määritellä se riippuvuustaulukkoon. Kuitenkin, tämä on hyvä tehdä koska et voi tietää, että yläkomponentti välittää aina saman refin, tai välittää yhden useista refeistä ehdollisesti. Joten Effectisi riippuisi siitä, mikä ref välitetään.

3. Vaihe: Lisää puhdistus tarvittaessa

Harkitse hieman erilaista esimerkkiä. Kirjoitat ChatRoom komponenttia, jonka tarvitsee yhdistää chat palvelimeen kun se ilmestyy. Sinulle annetaan createConnection() API joka palauttaa olion, jossa on connect() ja disconnect() metodit. Kuinka pidät komponentin yhdistettynä kun se näytetään käyttäjälle?

Aloita kirjoittamalla Effectin logiikka:

useEffect(() => {
const connection = createConnection();
connection.connect();
});

Olisi hidasta yhdistää chat -palvelimeen joka renderöinnin jälkeen, joten lisäät riippuvuustaulukon:

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

Effectin sisällä oleva koodi ei käytä yhtäkään propsia tai tilamuuttujaa, joten riippuvuustaulukkosi on [] (tyhjä). Tämä kertoo Reactille että suorittaa tämän koodin vain kun komponentti “mounttaa”, eli näkyy ensimmäistä kertaa näytöllä.

Kokeillaan koodin suorittamista:

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

Tämä Effecti suoritetaan vain mountissa, joten voit odottaa että "✅ Connecting..." tulostuu kerran konsoliin. Kuitenkin, jos tarkistat konsolin, "✅ Connecting..." tulostuu kaksi kertaa. Miksi se tapahtuu?

Kuvittele, että ChatRoom komponentti on osa isompaa sovellusta, jossa on useita eri näyttöjä. Käyttäjä aloittaa matkansa ChatRoom sivulta. Komponentti mounttaa ja kutsuu connection.connect(). Sitten kuvittele, että käyttäjä navigoi toiselle näytölle—esimerkiksi asetussivulle. ChatRoom komponentti unmounttaa. Lopuksi, käyttäjä painaa Takaisin -nappia ja ChatRoom mounttaa uudelleen. Tämä yhdistäisi toiseen kertaan—mutta ensimmäistä yhdistämistä ei koskaan tuhottu! Kun käyttäjä navigoi sovelluksen läpi, yhteydet kasaantuisivat.

Tämän kaltaiset bugit voivat helposti jäädä huomiotta ilman raskasta manuaalista testaamista. Helpottaaksesi näiden löytämistä, React kehitysvaiheessa remounttaa jokaisen komponentin kerran heti mountin jälkeen. Nähdessäsi "✅ Connecting..." tulostuksen kahdesti, huomaat helposti ongelman: koodisi ei sulje yhteyttä kun komponentti unmounttaa.

Korjataksesi ongelman, palauta siivousfunktio Effectistäsi:

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

React kutsuu siivousfunktiotasi joka kerta ennen kuin Effectia suoritetaan uudelleen, ja kerran kun komponentti unmounttaa (poistetaan). Kokeillaan mitä tapahtuu kun siivousfunktio on toteutettu:

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

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

Nyt saat kolme tulostusta konsoliin kehitysvaiheessa:

  1. "✅ Connecting..."
  2. "❌ Disconnected."
  3. "✅ Connecting..."

Tämä on kehitysvaiheen oikea käyttäytyminen. Remounttaamalla komponenttisi, React varmistaa että navigointi pois ja takaisin ei riko koodiasi. Yhdistäminen ja sitten katkaiseminen on juuri se mitä pitäisi tapahtua! Kun toteutat siivouksen hyvin, käyttäjälle ei pitäisi olla näkyvissä eroa suorittamalla Effectiä kerran vs suorittamalla se, siivoamalla se ja suorittamalla se uudelleen. Ylimääräinen yhdistys/katkaisu pari on olemassa kehitysvaiheessa, koska React tutkii koodiasi virheiden löytämiseksi. Tämä on normaalia ja sinun ei tulisi yrittää saada sitä pois.

Tuotannossa, näkisit ainoastaan "✅ Connecting..." tulostuksen kerran. Remounttaaminen tapahtuu vain kehitysvaiheessa auttaaksesi sinua löytämään Effectit, joissa on siivousfunktio. Voit kytkeä Strict Mode:n pois päältä, jotta saat kehitysvaiheen toiminnon pois käytöstä, mutta suosittelemme että pidät sen päällä. Tämä auttaa sinua löytämään monia bugeja kuten yllä.

Miten käsittelet kahdesti toistuvan Effectin kehitysvaiheessa?

React tarkoituksella remounttaa komponenttisi kehitysvaiheessa auttaaksesi sinua löytämään bugeja kuten edellisessä esimerkissä. Oikea kysymys ei ole “miten suoritan Effectin kerran”, vaan “miten korjaan Effectini niin että se toimii remounttauksen jälkeen”.

Useiten vastaus on toteuttaa siivousfunktio. Siivousfunktion pitäisi pysäyttää tai peruuttaa se mitä Effect oli tekemässä. Yleinen sääntö on että käyttäjän ei pitäisi pystyä erottamaan Effectin suorittamista kerran (tuotannossa) ja setup → cleanup → setup sekvenssistä (mitä näet kehitysvaiheessa).

Useimmat Effectit jotka kirjoitat sopivat yhteen alla olevista yleisistä kuvioista.

Ei-React komponenttien ohjaaminen

Joskus tarvitset UI pienoisohjelmia, jotka eivät ole kirjoitettu Reactiin. Esimerkiksi, sanotaan että lisäät kartta-komponentin sivullesi. Sillä on setZoomLevel() metodi, ja haluat pitää zoom tason synkronoituna zoomLevel tilamuuttujan kanssa React koodissasi. Effectisi näyttäisi tältä:

useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);

Huomaa, että tässä tilanteessa siivousta ei tarvita. Kehitysvaiheessa React kutsuu Effectia kahdesti, mutta tässä se ei ole ongelma, koska setZoomLevel:n kutsuminen kahdesti samalla arvolla ei tee mitään. Se saattaa olla hieman hitaampaa, mutta tämä ei ole ongelma koska remounttaus tapahtuu kehitysvaiheessa eikä tuotannossa.

Jotkin API:t eivät salli kutsua niitä kahdesti peräkkäin. Esimerkiksi, sisäänrakennetun <dialog> elementin showModal metodi heittää virheen jos kutsut sitä kahdesti peräkkäin. Toteuta siivousfunktio, joka sulkee dialogin:

useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);

Kehitysvaiheessa Effectisi kutsuu showModal() metodia, jonka perään heti close(), ja sitten showModal() metodia uudelleen. Tämä on käyttäjälle sama kuin jos kutsuisit showModal() metodia vain kerran, kuten näet tuotannossa.

Tapahtumien tilaaminen

Jos Effectisi tilaavat jotain, siivousfunktiosi pitäisi purkaa tilaus:

useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);

Kehitysvaiheessa Effectisi kutsuu addEventListener() metodia, jonka perään heti removeEventListener() metodia, ja sitten addEventListener() metodia uudelleen samalla käsittelijällä. Joten aina on vain yksi aktiivinen tilaus kerrallaan. Tämä on käyttäjälle sama kuin jos kutsuisit addEventListener() metodia vain kerran, kuten näet tuotannossa.

Animaatioiden käynnistäminen

Jos Effectisi animoi jotain, siivousfunktiosi pitäisi palauttaa animaatio alkuperäiseen tilaan:

useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Käynnistä animaatio
return () => {
node.style.opacity = 0; // Palauta oletusarvoon
};
}, []);

Kehitysvaiheessa läpinäkyvyys asetetaan 1:een, sitten 0:aan, ja sitten 1:een uudelleen. Tämä pitäisi olla käyttäjälle sama kuin jos asettaisit sen suoraan 1:een, joka olisi mitä tapahtuu tuotannossa. Jos käytät kolmannen osapuolen animaatiokirjastoa joka tukee tweenausta (engl. tweening), siivousfunktion pitäisi palauttaa tweenin aikajana alkuperäiseen tilaan.

Tiedon haku

Jos Effectisi hakee jotain, siivousfunktiosi pitäisi joko perua haku tai sivuuttaa sen tulos:

useEffect(() => {
let ignore = false;

async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}

startFetching();

return () => {
ignore = true;
};
}, [userId]);

Et voi “peruuttaa” verkkopyyntöä joka on jo tapahtunut, mutta siivousfunktiosi pitäisi varmistaa että pyyntö joka ei ole enää tarpeellinen ei vaikuta sovellukseesi. Jos userId muuttuu 'Alice':sta 'Bob':ksi, siivousfunktio varmistaa että 'Alice' vastaus jätetään huomiotta vaikka se vastaanotettaisiin 'Bob':n vastauksen jälkeen.

Kehitysvaiheessa, näet kaksi verkkopyyntöä Network välilehdellä. Tässä ei ole mitään vikaa. Yllä olevan menetelmän mukaan, ensimmäinen Effecti poistetaan välittömästi, joten sen kopio ignore muuttujasta asetetaan true:ksi. Joten vaikka onkin ylimääräinen pyyntö, se ei vaikuta tilaan kiitos if (!ignore) tarkistuksen.

Tuotannossa tulee tapahtumaan vain yksi pyyntö. Jos kehitysvaiheessa toinen pyyntö häiritsee sinua, paras tapa on käyttää ratkaisua joka deduplikoi pyynnöt ja asettaa niiden vastaukset välimuistiin komponenttien välillä:

function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...

Tämä ei vain paranna kehityskokemusta, vaan myös saa sovelluksesi tuntumaan nopeammalta. Esimerkiksi, käyttäjän ei tarvitse odottaa että jotain dataa ladataan uudelleen kun painaa Takaisin -painiketta, koska se on välimuistissa. Voit joko rakentaa tällaisen välimuistin itse tai effecteissa manuaalisen datahaun sijaan käyttää jotain olemassa olevaa vaihtoehtoa.

Syväsukellus

Mitkä ovat hyviä vaihtoehtoja datan hakemiseen effecteissa?

fetch kutsujen kirjoittaminen Effecteissa on suosittu tapa hakea dataa, erityisesti täysin asiakaspuolen sovelluksissa. Tämä on kuitenkin hyvin manuaalinen tapa ja sillä on merkittäviä haittoja:

  • Effecteja ei ajeta palvelimella. Tämä tarkoittaa, että palvelimella renderöity HTML sisältää vain lataus -tilan ilman dataa. Asiakkaan tietokoneen pitää ladata koko JavaScript ja renderöidä sovellus, jotta se huomaa, että nyt sen täytyy ladata dataa. Tämä ei ole erityisen tehokasta.
  • Hakeminen Effectissa tekee “verkkovesiputouksien” toteuttamisesta helppoa. Renderöit ylemmän komponentin, se hakee jotain dataa, renderöit lapsikomponentit, ja sitten ne alkavat hakea omaa dataansa. Jos verkkoyhteys ei ole erityisen nopea, tämä on huomattavasti hitaampaa kuin jos kaikki datat haettaisiin yhtäaikaisesti.
  • Hakeminen suoraan Effecteissa useiten tarkoittaa ettet esilataa tai välimuista dataa. Esimerkiksi, jos komponentti poistetaan ja sitten liitetään takaisin, se joutuu hakemaan datan uudelleen.
  • Se ei ole kovin ergonomista. Pohjakoodia on aika paljon kirjoittaessa fetch kutsuja tavalla, joka ei kärsi bugeista kuten kilpailutilanteista.

Tämä lista huonoista puolista ei koske pelkästään Reactia. Se pätee mihin tahansa kirjastoon kun dataa haetaan mountissa. Kuten reitityksessä, datan hakeminen ei ole helppoa tehdä hyvin, joten suosittelemme seuraavia lähestymistapoja:

  • Jos käytät frameworkia, käytä sen sisäänrakennettua datan hakemiseen tarkoitettua mekanismia. Modernit React frameworkit sisältävät tehokkaita datan hakemiseen tarkoitettuja mekanismeja, jotka eivät kärsi yllä mainituista ongelmista.
  • Muussa tapauksessa, harkitse tai rakenna asiakaspuolen välimuisti. Suosittuja avoimen lähdekoodin ratkaisuja ovat React Query, useSWR, ja React Router 6.4+. Voit myös rakentaa oman ratkaisusi, jolloin käytät Effecteja alustana mutta lisäät logiikkaa pyyntöjen deduplikointiin, vastausten välimuistitukseen ja verkkovesiputousten välttämiseen (esilataamalla dataa tai nostamalla datan vaatimukset reiteille).

Voit jatkaa datan hakemista suoraan Effecteissa jos nämä lähestymistavat eivät sovi sinulle.

Analytiikan lähettäminen

Harkitse tätä koodia, joka lähettää analytiikkatapahtuman sivun vierailusta:

useEffect(() => {
logVisit(url); // Lähettää POST pyynnön
}, [url]);

Kehitysvaiheessa logVisit kutsutaan kahdesti jokaiselle URL:lle, joten saattaa olla houkuttelevaa tämän välttämistä. Suosittelemme pitämään tämän koodin sellaisenaan. Kuten aiemmissa esimerkeissä, ei ole käyttäjän näkökulmasta havaittavaa eroa siitä, ajetaanko se kerran vai kahdesti. Käytännöllisestä näkökulmasta logVisit:n ei tulisi tehdä mitään kehitysvaiheessa, koska et halua, että kehityskoneiden lokit vaikuttavat tuotantotilastoihin. Komponenttisi remounttaa joka kerta kun tallennat sen tiedoston, joten se lähettäisi ylimääräisiä vierailuja kehitysvaiheessa joka tapauksessa.

Tuotannossa ei ole kaksoiskappaleita vierailulokeista.

Analytiikkatapahtumien debuggauukseen voit joko julkaista sovelluksen testiympäristöön (joka suoritetaan tuotantotilassa) tai väliaikaisesti poistaa käytöstä Strict Mode:n ja sen kehitysvaiheessa olevat remounttaus-tarkistukset. Voit myös lähettää analytiikkaa reitityksen tapahtumakäsittelijöistä Effectien sijaan. Entistäkin tarkemman analytiikan lähettämiseen voit käyttää Intersection Observer API:a, jotka auttavat seuraamaan, mitkä komponentit ovat näkyvissä ja kuinka kauan.

Ei ole Effect: Sovelluksen alustaminen

Jokin logiikka tulisi suorittaa vain kerran kun sovellus käynnistyy. Voit laittaa sen komponentin ulkopuolelle:

if (typeof window !== 'undefined') { // Tarkista suoritetaanko selaimessa
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

Tämä takaa, että tällainen logiikka suoritetaan vain kerran selaimen lataamisen jälkeen.

Ei ole Effect: Tuotteen ostaminen

Joksus, vaikka kirjoittaisit siivousfunktion, ei ole tapaa estää käyttäjälle näkyviä seurauksia Effectin kahdesti suorittamisesta. Esimerkiksi, joskus Effecti voi lähettää POST pyynnön kuten tuotteen ostamisen:

useEffect(() => {
// 🔴 Väärin: Tämä Effecti suoritetaan kahdesti tuotannossa, paljastaen ongelman koodissa.
fetch('/api/buy', { method: 'POST' });
}, []);

Et halua ostaa tuotetta kahdesti. Kuitenkin, tämä on myös syy miksi et halua laittaa tätä logiikkaa Effectiin. Mitä jos käyttäjä menee toiselle sivulle ja tulee takaisin? Effectisi suoritetaan uudelleen. Et halua ostaa tuotetta koska käyttäjä vieraili sivulla; haluat ostaa sen kun käyttäjä painaa Osta -nappia.

Ostaminen ei aiheutunut renderöinnin takia. Se aiheutuu tietyn vuorovaikutuksen takia. Se suoritetaan vain kerran koska vuorovaikutus (napsautus) tapahtuu vain kerran. Poista Effecti ja siirrä /api/buy pyyntö Osta -painkkeen tapahtumakäsittelijään:

function handleClick() {
// ✅ Ostaminen on tapahtuma, koska se aiheutuu tietyn vuorovaikutuksen seurauksena.
fetch('/api/buy', { method: 'POST' });
}

Tämä osoittaa, että jos remounttaus rikkoo sovelluksen logiikkaa, tämä usein paljastaa olemassa olevia virheitä. Käyttäjän näkökulmasta, sivulla vierailu ei pitäisi olla sen erilaisempaa kuin vierailu, linkin napsautus ja sitten Takaisin -painikkeen napsauttaminen. React varmistaa, että komponenttisi eivät riko tätä periaatetta kehitysvaiheessa remounttaamalla niitä kerran.

Laitetaan kaikki yhteen

Tämä hiekkalaatikko voi auttaa “saamaan tunteen” siitä, miten Effectit toimivat käytännössä.

Tämä esimerkki käyttää setTimeout funktiota aikatauluttaakseen konsolilokiin syötetyn tekstin ilmestyvän kolmen sekunnin kuluttua Effectin suorittamisen jälkeen. Siivoamisfunktio peruuttaa odottavan aikakatkaisun. Aloita painamalla “Mount the component”:

import { useState, useEffect } from 'react';

function Playground() {
  const [text, setText] = useState('a');

  useEffect(() => {
    function onTimeout() {
      console.log('⏰ ' + text);
    }

    console.log('🔵 Schedule "' + text + '" log');
    const timeoutId = setTimeout(onTimeout, 3000);

    return () => {
      console.log('🟡 Cancel "' + text + '" log');
      clearTimeout(timeoutId);
    };
  }, [text]);

  return (
    <>
      <label>
        What to log:{' '}
        <input
          value={text}
          onChange={e => setText(e.target.value)}
        />
      </label>
      <h1>{text}</h1>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Unmount' : 'Mount'} the component
      </button>
      {show && <hr />}
      {show && <Playground />}
    </>
  );
}

Näet aluksi kolme eri lokia: Schedule "a" log, Cancel "a" log, ja Schedule "a" log uudelleen. Kolme sekuntia myöhemmin lokiin ilmestyy viesti a. Kuten opit aiemmin tällä sivulla, ylimääräinen schedule/cancel pari tapahtuu koska React remounttaa komponentin kerran kehitysvaiheessa varmistaakseen, että olet toteuttanut siivouksen hyvin.

Nyt muokkaa syöttölaatikon arvoksi abc. Jos teet sen tarpeeksi nopeasti, näet Schedule "ab" log viestin, jonka jälkeen Cancel "ab" log ja Schedule "abc" log. React siivoaa aina edellisen renderöinnin Effectin ennen seuraavan renderöinnin Effectiä. Tämä on syy miksi vaikka kirjoittaisit syöttölaatikkoon nopeasti, aikakatkaisuja on aina enintään yksi kerrallaan. Muokkaa syöttölaatikkoa muutaman kerran ja katso konsolia saadaksesi käsityksen siitä, miten Effectit siivotaan.

Kirjoita jotain syöttölaatikkoon ja heti perään paina “Unmount the component”. Huomaa kuinka unmounttaus siivoaa viimeisen renderöinnin Effectin. Tässä esimerkissä se tyhjentää viimeisen aikakatkaisun ennen kuin se ehtii käynnistyä.

Lopuksi, muokkaa yllä olevaa komponenttia ja kommentoi siivousfunktio, jotta ajastuksia ei peruuteta. Kokeile kirjoittaa abcde nopeasti. Mitä odotat tapahtuvan kolmen sekuntin kuluttua? Tulisiko console.log(text) aikakatkaisussa tulostamaan viimeisimmän text:n ja tuottamaan viisi abcde lokia? Kokeile tarkistaaksesi intuitiosi!

Kolmen sekuntin jälkeen lokeissa tulisi näkyä (a, ab, abc, abcd, ja abcde) viiden abcde lokin sijaan. Kukin Effecti nappaa text:n arvon vastaavasta renderöinnistä. Se ei ole väliä, että text tila muuttui: Effecti renderöinnistä text = 'ab' näkee aina 'ab'. Toisin sanottuna, Effectit jokaisesta renderöinnistä ovat toisistaan erillisiä. Jos olet kiinnostunut siitä, miten tämä toimii, voit lukea closureista.

Syväsukellus

Kullakin renderillä on sen omat Effectit

Voit ajatella useEffect:ia “liittävän” palan toiminnallisuutta osana renderöinnin tulosta. Harkitse tätä Effectiä:

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

return <h1>Welcome to {roomId}!</h1>;
}

Katsotaan mitä oikeasti tapahtuu kun käyttäjä liikkuu sovelluksessa.

Alustava renderöinti

Käyttäjä vierailee <ChatRoom roomId="general" />. Katsotaan mielikuvitustilassa roomId arvoksi 'general':

// JSX ensimäisellä renderöinnillä (roomId = "general")
return <h1>Welcome to general!</h1>;

Effecti on myös osa renderöinnin tulosta. Ensimmäisen renderöinnin Effecti muuttuu:

// Effecti ensimäisellä renderöinnillä (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Riippuvuudet ensimäisellä renderöinnillä (roomId = "general")
['general']

React suorittaa tämän Effectin, joka yhdistää 'general' keskusteluhuoneeseen.

Uudelleen renderöinti samoilla riippuvuuksilla

Sanotaan, että <ChatRoom roomId="general" /> renderöidään uudelleen. JSX tuloste pysyy samana:

// JSX toisella renderöinnillä (roomId = "general")
return <h1>Welcome to general!</h1>;

React näkee, että renderöinnin tuloste ei ole muuttunut, joten se ei päivitä DOM:ia.

Effecti toiselle renderöinnille näyttää tältä:

// Effecti toisella renderöinnillä (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Riippuvuudet toisella renderöinnillä (roomId = "general")
['general']

React vertaa ['general']:a toiselta renderöinniltä ensimmäisen renderöinnin ['general'] kanssa. Koska kaikki riippuvuudet ovat samat, React jättää huomiotta toisen renderöinnin Effectin. Sitä ei koskaan kutsuta.

Uudelleen renderöinti eri riippuvuuksilla

Sitten, käyttäjä vierailee <ChatRoom roomId="travel" />. Tällä kertaa komponentti palauttaa eri JSX:ää:

// JSX kolmannella renderöinnillä (roomId = "travel")
return <h1>Welcome to travel!</h1>;

React päivittää DOM:in muuttamalla "Welcome to general" lukemaan "Welcome to travel".

Effecti kolmannelle renderöinnille näyttää tältä:

// Effecti kolmannella renderöinnillä (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Riippuvuudet kolmannella renderöinnillä (roomId = "travel")
['travel']

React vertaa ['travel']:ia kolmannelta renderöinniltä toiselta renderöinnin ['general'] kanssa. Yksi riippuvuus on erilainen: Object.is('travel', 'general') on false. Effectiä ei voi jättää huomiotta.

Ennen kuin React voi ottaa käyttöön kolmannen renderöinnin Effectin, sen täytyy siivota viimeisin Effecti joka suoritettiin. Toisen renderöinnin Effecti ohitettiin, joten Reactin täytyy siivota ensimmäisen renderöinnin Effecti. Jos selaat ylös ensimmäiseen renderöintiin, näet että sen siivous kutsuu createConnection('general'):lla luodun yhteyden disconnect() metodia. Tämä irroittaa sovelluksen 'general' keskusteluhuoneesta.

Sen jälkeen React suorittaa kolmannen renderöinnin Effectin. Se yhdistää sovelluksen 'travel' keskusteluhuoneeseen.

Unmount

Lopuksi, sanotaan, että käyttäjä siirtyy pois ja ChatRoom komponentti unmounttaa. React suorittaa viimeisen Effectin siivousfunktion. Viimeinen Effecti oli kolmannen renderöinnin. Kolmannen renderöinnin siivousfunktio tuhoaa createConnection('travel') yhteyden. Joten sovellus irroittaa itsensä 'travel' keskusteluhuoneesta.

Kehitysvaiheen käyttäytymiset

Kun Strict Mode on käytössä, React remounttaa jokaisen komponentin kerran mountin jälkeen (tila ja DOM säilytetään). Tämä helpottaa löytämään Effecteja jotka tarvitsevat siivousfunktiota ja paljastaa bugeja kuten kilpailutilanteita (engl. race conditions). Lisäksi, React remounttaa Effectit joka kerta kun tallennat tiedoston kehitysvaiheessa. Molemmat näistä käyttäytymisistä tapahtuu ainoastaan kehitysvaiheessa.

Kertaus

  • Toisin kuin tapahtumat, Effectit aiheutuvat renderöinnin seurauksena tietyn vuorovaikutuksen sijaan.
  • Effectien avulla voit synkronoida komponentin jonkin ulkoisen järjestelmän kanss (kolmannen osapuolen API:n, verkon, jne.).
  • Oletuksena, Effectit suoritetaan jokaisen renderöinnin jälkeen (mukaan lukien ensimmäinen renderöinti).
  • React ohittaa Effectin jos kaikki sen riippuvuudet ovat samat kuin viimeisellä renderöinnillä.
  • Et voi “valita” riippuvuuksiasi. Ne määräytyvät Effectin sisällä olevan koodin mukaan.
  • Tyhjä riippuvuustaulukko ([]) vastaa komponentin “mounttaamista”, eli sitä kun komponentti lisätään näytölle.
  • Kun Strict Mode on käytössä, React mounttaa komponentit kaksi kertaa (vain kehitysvaiheessa!) stressitestataksesi Effecteja.
  • Jos Effecti rikkoutuu remountin takia, sinun täytyy toteuttaa siivousfunktio.
  • React kutsuu siivousfunktiota ennen kuin Effectiasi suoritetaan seuraavan kerran, ja unmountin yhteydessä.

Haaste 1 / 4:
Kohdenna kenttään mountattaessa

Tässä esimerkissä, lomake renderöi <MyInput /> komponentin.

Käytä inputin focus() metodia, jotta MyInput komponentti automaattisesti kohdentuu kun se ilmestyy näytölle. Alhaalla on jo kommentoitu toteutus, mutta se ei toimi täysin. Selvitä miksi se ei toimi ja korjaa se. (Jos olet tutustunut autoFocus attribuuttiin, kuvittele, että sitä ei ole olemassa: me toteutamme saman toiminnallisuuden alusta alkaen.)

import { useEffect, useRef } from 'react';

export default function MyInput({ value, onChange }) {
  const ref = useRef(null);

  // TODO: Tämä ei ihan toimi. Korjaa se.
  // ref.current.focus()    

  return (
    <input
      ref={ref}
      value={value}
      onChange={onChange}
    />
  );
}

Tarkistaaksesi, että ratkaisusi toimii, paina “Show form” ja tarkista, että kenttä kohdentuu (se muuttuu korostetuksi ja kursori asettuu siihen). Paina “Hide form” ja “Show form” uudelleen. Tarkista, että kenttä on korostettu uudelleen.

MyInput pitäisi kohdentua mounttauksen yhteydessä eikä jokaisen renderöinnin jälkeen. Varmistaaksesi, että toiminnallisuus on oikein, paina “Show form”:ia ja sitten paina toistuvasti “Make it uppercase” valintaruutua. Valintaruudun klikkaaminen ei pitäisi kohdentaa kenttää yllä.