Synkronointi Efekteillä

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ä. Efekti mahdollistavat koodin suorittamisen renderöinnin jälkeen, jotta voit synkronoida komponentin jonkin ulkoisen järjestelmän kanssa Reactin ulkopuolella.

Tulet oppimaan

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

Mitä Efektit ovat ja miten ne eroavat tapahtumista?

Ennen kuin siirrytään Efekteihin, 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.

  • Tapahtumankäsittelijät (esitellään Interaktiivisuuden lisäämisessä) ovat komponenttien sisäisiä funktioita, jotka tekevät asioita sen sijaan, että vain laskisivat asioita. Tapahtumankä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. Tapahtumankä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.

Efektien 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 Efekti, koska se on tehtävä riippumatta siitä, mikä vuorovaikutus aiheutti komponentin näkyvyyden. Efektit 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ä, “Efektillä”:llä viittaamme Reactin määritelmään, eli sivuvaikutukseen, joka aiheutuu renderöinnistä. Viittaaksemme laajempaan ohjelmointikäsitteeseen, sanomme “sivuvaikutus”.

Et välttämättä tarvitse Efektiä

Älä kiiruhda lisäämään Efektejä komponentteihisi. Pidä mielessä, että Efektit 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 Efektisi vain muuttaa tilaa perustuen toiseen tilaan, voit ehkä jättää Efektin pois.

Miten kirjoitat Efektin

Kirjoittaaksesi Efektin, seuraa näitä kolmea vaihetta:

  1. Määrittele Efekti. Oletuksena, Efektisi suoritetaan jokaisen renderöinnin jälkeen.
  2. Määrittele Efektin riippuvuudet. Useimmat Efektit 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 Efektien 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 Efekti

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

import { useEffect } from 'react';

Sitten kutsu sitä komponentin yläpuolella ja laita koodia Efektin 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ää Efektiä 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 useEffect:lla 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 Efektiin, annat Reactin päivittää ensin ruudun. Sitten Efektisi 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 Efektisi. Lopuksi, Efektisi 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 Efektit 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);
});

Efektit suoritetaan renderöinnin johdosta. Tilan asettaminen aiheuttaa renderöinnin. Tilan asettaminen välittömästi Efektissä on kuin pistäisi jatkojohdon kiinni itseensä. Efekti suoritetaan, se asettaa tilan, joka aiheuttaa uudelleen renderöinnin, joka aiheuttaa Efektin suorittamisen, joka asettaa tilan uudelleen, joka aiheuttaa uudelleen renderöinnin, ja niin edelleen.

Efektien 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ää Efektin pois.

2. Vaihe: Määrittele Efektin riippuvuudet

Oletuksena Efektit 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 Efektin 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 Efektin 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ä Efektin 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 riippuvuuslistalle:

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 Efektin uudelleen suorittaminen jos isPlaying on sama kuin se oli edellisellä renderöinnillä. Tämän muutoksen jälkeen, tekstikenttään kirjoittaminen ei aiheuta Efektin 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"
      />
    </>
  );
}

Riippuvuuslista voi sisältää useita riippuvuuksia. React ohittaa Efektin 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 Efektin käyttää jotain arvoa, mutta et halua suorittaa Efektiä uudelleen kun se muuttuu, sinun täytyy muokata Efektin koodia itse jotta se ei “tarvitse” tätä riippuvuutta.

Sudenkuoppa

Käyttäytyminen ilman riippuvuuslistaa 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ä Efekti käyttää sekä ref että isPlaying:ä, mutta vain isPlaying on määritelty riippuvuuslistassa:

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 Efektin uudelleen suorittamista. Siksi ei ole merkityksellistä onko se määritelty riippuvuuslistassa 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 riippuvuuslistalle. 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 Efektisi 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 Efektin 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();
}, []);

Efektin sisällä oleva koodi ei käytä yhtäkään propsia tai tilamuuttujaa, joten riippuvuuslistasi 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ä Efektin 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 Efektistäsi:

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

React kutsuu siivousfunktiotasi joka kerta ennen kuin Efektiä 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 Efektiä 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 Efektit, 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 Efektin kehitysvaiheessa?

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

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

Useimmat Efektit 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. Efektisi 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 Efektiä 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 Efektisi 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 Efektisi 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 Efektisi 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 Efektisi 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 Efektisi 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 Efektisi 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 Efekteissä manuaalisen datahaun sijaan käyttää jotain olemassa olevaa vaihtoehtoa.

Syväsukellus

Mitkä ovat hyviä vaihtoehtoja datan hakemiseen Efekteissä?

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

  • Efektejä 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 Efektissä 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 Efekteissä 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 Efektejä 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 Efekteissä 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 Tapahtumankäsittelijöistä Efektien 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 Efekti: 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 Efekti: Tuotteen ostaminen

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

useEffect(() => {
// 🔴 Väärin: Tämä Efekti 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 Efektiin. Mitä jos käyttäjä menee toiselle sivulle ja tulee takaisin? Efektisi 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 Efekti ja siirrä /api/buy pyyntö Osta -painkkeen Tapahtumankä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 Efektit toimivat käytännössä.

Tämä esimerkki käyttää setTimeout funktiota aikatauluttaakseen konsolilokiin syötetyn tekstin ilmestyvän kolmen sekunnin kuluttua Efektin 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 Efektin ennen seuraavan renderöinnin Efektiä. 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 Efektit siivotaan.

Kirjoita jotain syöttölaatikkoon ja heti perään paina “Unmount the component”. Huomaa kuinka unmounttaus siivoaa viimeisen renderöinnin Efektin. 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 Efekti nappaa text:n arvon vastaavasta renderöinnistä. Se ei ole väliä, että text tila muuttui: Efekti renderöinnistä text = 'ab' näkee aina 'ab'. Toisin sanottuna, Efektit jokaisesta renderöinnistä ovat toisistaan erillisiä. Jos olet kiinnostunut siitä, miten tämä toimii, voit lukea closureista.

Syväsukellus

Kullakin renderillä on sen omat Efektit

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

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>;

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

// Efekti 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 Efekti, 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.

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

// Efekti 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 Efektin. 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".

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

// Efekti 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. Efektiä ei voi jättää huomiotta.

Ennen kuin React voi ottaa käyttöön kolmannen renderöinnin Efektin, sen täytyy siivota viimeisin Efekti joka suoritettiin. Toisen renderöinnin Efekti ohitettiin, joten Reactin täytyy siivota ensimmäisen renderöinnin Efekti. 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 Efektin. Se yhdistää sovelluksen 'travel' keskusteluhuoneeseen.

Unmount

Lopuksi, sanotaan, että käyttäjä siirtyy pois ja ChatRoom komponentti unmounttaa. React suorittaa viimeisen Efektin siivousfunktion. Viimeinen Efekti 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 Efektit joka kerta kun tallennat tiedoston kehitysvaiheessa. Molemmat näistä käyttäytymisistä tapahtuu ainoastaan kehitysvaiheessa.

Kertaus

  • Toisin kuin tapahtumat, Efektit aiheutuvat renderöinnin seurauksena tietyn vuorovaikutuksen sijaan.
  • Efektien avulla voit synkronoida komponentin jonkin ulkoisen järjestelmän kanss (kolmannen osapuolen API:n, verkon, jne.).
  • Oletuksena, Efektit suoritetaan jokaisen renderöinnin jälkeen (mukaan lukien ensimmäinen renderöinti).
  • React ohittaa Efektin jos kaikki sen riippuvuudet ovat samat kuin viimeisellä renderöinnillä.
  • Et voi “valita” riippuvuuksiasi. Ne määräytyvät Efektin sisällä olevan koodin mukaan.
  • Tyhjä riippuvuuslista ([]) 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 Efekti rikkoutuu remountin takia, sinun täytyy toteuttaa siivousfunktio.
  • React kutsuu siivousfunktiota ennen kuin Efektiasi 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ä.