Tilan säilyttäminen ja nollaus

Tila on eristetty komponenttien välillä. React pitää kirjaa siitä, mikä tila kuuluu mihinkin komponenttiin sen perusteella, mikä on niiden paikka käyttöliittymäpuussa. Voit hallita, milloin tila säilytetään ja milloin se nollataan uudelleen renderöintiä varten.

Tulet oppimaan

  • Miten React “näkee” komponentin rakenteen
  • Milloin React päättää säilyttää tai nollata tilan
  • Miten pakottaa React nollaamaan komponentin tila
  • Miten avaimet ja tyypit vaikuttavat tilan säilyttämiseen

Käyttöliittymäpuu

Selaimet käyttävät monia puumalleja käyttöliittymän mallintamiseen. DOM edustaa HTML-elementtejä, CSSOM tekee saman CSS:lle. On olemassa jopa saavutettavuuspuu!

React käyttää myös puurakenteita käyttöliittymäsi hallintaan ja mallintamiseen. React rakentaa UI puita JSX koodistasi. Sitten React DOM päivittää selaimen DOM elementit vastaamaan tuota UI puuta. (React Native kääntää nämä puut näkymiksi, jotka voidaan näyttää puhelinalustoilla.)

Kaavio, jossa on kolme vaakasuoraan sijoitettua osaa. Ensimmäisessä osassa on kolme pystysuoraan pinottua suorakulmiota, joissa on merkinnät 'Komponentti A', 'Komponentti B' ja 'Komponentti C'. Seuraavaan osioon siirtyy nuoli, jonka yläpuolella on React-logo ja jossa on merkintä 'React'. Keskimmäisessä osassa on komponenttien puu, jonka juuressa on merkintä 'A' ja kahdessa alakomponentissa merkinnät 'B' ja 'C'. Seuraavaan osioon siirrytään jälleen nuolella, jonka yläosassa on React-logo ja jossa on merkintä 'React'. Kolmas ja viimeinen osio on selaimen rautalankamalli, joka sisältää kahdeksan solmun puun, josta on korostettu vain osajoukko (joka osoittaa keskimmäisen osion alipuun).
Kaavio, jossa on kolme vaakasuoraan sijoitettua osaa. Ensimmäisessä osassa on kolme pystysuoraan pinottua suorakulmiota, joissa on merkinnät 'Komponentti A', 'Komponentti B' ja 'Komponentti C'. Seuraavaan osioon siirtyy nuoli, jonka yläpuolella on React-logo ja jossa on merkintä 'React'. Keskimmäisessä osassa on komponenttien puu, jonka juuressa on merkintä 'A' ja kahdessa alakomponentissa merkinnät 'B' ja 'C'. Seuraavaan osioon siirrytään jälleen nuolella, jonka yläosassa on React-logo ja jossa on merkintä 'React'. Kolmas ja viimeinen osio on selaimen rautalankamalli, joka sisältää kahdeksan solmun puun, josta on korostettu vain osajoukko (joka osoittaa keskimmäisen osion alipuun).

Komponenteista React luo käyttöliittymäpuun, jota React DOM käyttää renderöidäkseen DOM:n

Tila on sidottu sijaintiin puussa

Kun annat komponentille tilan, saatat ajatella, että tila “asuu” komponentin sisällä. Mutta tila oikeasti pidetään Reactin sisällä. React yhdistää jokaisen hallussa olevan tilatiedon oikeaan komponenttiin sen mukaan, missä kohtaa käyttöliittymäpuuta kyseinen komponentti sijaitsee.

Tässä esimerkissä on vain yksi <Counter /> JSX tagi, mutta se on renderöity kahdessa eri kohdassa:

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Miltä tämä näyttäisi puuna:

Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi lasta. Kummankin alakomponentin nimi on 'Counter', ja molemmat sisältävät tilan 'count', jonka arvo on 0.
Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi lasta. Kummankin alakomponentin nimi on 'Counter', ja molemmat sisältävät tilan 'count', jonka arvo on 0.

React puu

Nämä ovat kaksi erillistä laskuria, koska ne on renderöity niiden omissa sijainneissa puussa. Sinun ei tarvitse muistaa näitä sijainteja käyttääksesi Reactia, mutta voi olla hyödyllistä ymmärtää miksi se toimii.

Reactissa, kukin komponentti ruudulla omaa eristetyn tilan. Esimerkiksi, jos renderöiti kaksi Counter komponenttia vierekkäin, molemmilla on niiden omat, itsenäiset, score ja hover tilat.

Kokeile klikata molempia laskureita ja huomaat, etteivät ne vaikuta toisiinsa:

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Kuten voit nähdä, kun toista laskuria päivitetään, vain sen komponentin tila päivittyy:

Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi lasta. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikeanpuoleinen komponentti on merkitty 'Counter' ja sisältää tilan 'count', jonka arvo on 1. Oikean lapsen tila on korostettu keltaisella merkiksi siitä, että sen arvo on päivittynyt.
Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi lasta. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikeanpuoleinen komponentti on merkitty 'Counter' ja sisältää tilan 'count', jonka arvo on 1. Oikean lapsen tila on korostettu keltaisella merkiksi siitä, että sen arvo on päivittynyt.

Tilan päivittäminen

React pitää tilan muistissa niin kauan kuin renderlit samaa komponenttia samassa sijainnissa. Tämän nähdäksesi, korota molempia laskureita ja sitten poista toinen komponentti poistamalla valinta “Render the second counter” valintaruudusta, ja sitten lisää se takaisin valitsemalla se uudelleen:

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Render the second counter
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Huomaa, kun lopetat toisen laskurin renderöimisen, sen tila katoaa täysin. Tämä tapahtuu koska kun React poistaa komponentin, se poistaa sen tilan.

Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi alakomponenttia. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikeanpuoleinen komponentti puuttuu, ja sen tilalla on keltainen poof -kuva, joka korostaa puusta poistettavaa komponenttia.
Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi alakomponenttia. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikeanpuoleinen komponentti puuttuu, ja sen tilalla on keltainen poof -kuva, joka korostaa puusta poistettavaa komponenttia.

Komponentin poistaminen

Kun valitset “Render the second counter”, toinen Counter ja sen tula alustetaan tyjästä (score = 0) ja lisätään DOM:iin.

Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi alakomponenttia. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikea komponentti on merkitty 'Counter' ja sisältää tilan 'count', jonka arvo on 0. Koko oikea solmu on korostettu keltaisella, mikä osoittaa, että se on juuri lisätty puuhun.
Kaavio React-komponenttien puusta. Juurinoodilla on merkintä 'div', ja sillä on kaksi alakomponenttia. Vasemmanpuoleinen komponentti on merkitty 'Counter', ja se sisältää tilan 'count', jonka arvo on 0. Oikea komponentti on merkitty 'Counter' ja sisältää tilan 'count', jonka arvo on 0. Koko oikea solmu on korostettu keltaisella, mikä osoittaa, että se on juuri lisätty puuhun.

Komponentin lisääminen

React säilyttää komponentin tilan niin kauan kuin se on renderöity sen sijainnissa käyttöliittymäpuussa. Jos se poistetaan tai toinen komponentti renderöidään sen sijaintiin, React hävittää sen tilan.

Sama komponentti samassa sijainnissa säilyttää tilan

Tässä esimerkissä on kaksi eri <Counter /> tagia:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Kun valitset tai tyhjäät valintaruudun, laskurin tila ei poistu. Riippuen onko isFancy true vai false, <Counter /> on aina ensimmäinen App komponentin palauttaman div:n lapsi:

Kaavio, jossa on kaksi osiota, jotka on erotettu toisistaan nuolella. Kumpikin osio sisältää komponenttien asettelun, jonka pääosassa on merkintä 'App' ja tila, jossa on merkintä isFancy. Tällä komponentilla on yksi lapsi, jonka nimi on 'div', mikä johtaa isFancyn sisältävään propsi-kuplaan (violetilla korostettuna), joka siirtyy ainoalle alakomponentille. Alakomponentin nimi on 'Counter' ja se sisältää tilan, jonka nimi on 'count' ja arvo 3 molemmissa kaavioissa. Kaavion vasemmassa osassa mitään ei ole korostettu ja isFancy-vanhemman tilan arvo on false. Kaavion oikeanpuoleisessa osassa isFancy-vanhempien tilan arvo on muuttunut arvoon true, ja se on korostettu keltaisella, samoin kuin alapuolella oleva propsi-kupla, jonka isFancy-arvo on myös muuttunut arvoon true.
Kaavio, jossa on kaksi osiota, jotka on erotettu toisistaan nuolella. Kumpikin osio sisältää komponenttien asettelun, jonka pääosassa on merkintä 'App' ja tila, jossa on merkintä isFancy. Tällä komponentilla on yksi lapsi, jonka nimi on 'div', mikä johtaa isFancyn sisältävään propsi-kuplaan (violetilla korostettuna), joka siirtyy ainoalle alakomponentille. Alakomponentin nimi on 'Counter' ja se sisältää tilan, jonka nimi on 'count' ja arvo 3 molemmissa kaavioissa. Kaavion vasemmassa osassa mitään ei ole korostettu ja isFancy-vanhemman tilan arvo on false. Kaavion oikeanpuoleisessa osassa isFancy-vanhempien tilan arvo on muuttunut arvoon true, ja se on korostettu keltaisella, samoin kuin alapuolella oleva propsi-kupla, jonka isFancy-arvo on myös muuttunut arvoon true.

App komponentin tilan päivittäminen ei nollaa Counter:ia, koska Counter pysyy samassa sijainnissa

Se on sama komponentti samassa sijainnissa, joten Reactin näkökulmasta se on sama laskuri.

Sudenkuoppa

Muista, että Reactin kannalta tärkeintä on sen sijainti käyttöliittymäpuussa, ei JSX merkinnässä Tällä komponentilla on kaksi return lausetta eri <Counter /> JSX tageilla sekä if lauseiden sisällä että ulkopuolella:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Saatat olettaa tilan nollautuvan kun valitset valintaruudun, mutta se ei nollaudu! Tämä tapahtuu, koska molemmat <Counter /> tageista renderöidään samaan sijaintiin. React ei tiedä mihin sijoitat ehtolauseet funktiossasi. Ainoa mitä se “näkee” on puun, jonka palautat.

Molemmissa tapauksissa, App komponentti palauttaa <div>:n jossa on <Counter /> ensimmäisenä lapsena. Tämän takia React katsoo niiden olevan samat <Counter /> komponentit. Voit ajatella, että niillä on sama “osoite”: juuren ensimmäisen lapsen ensimmäinen lapsi. Näin React sovittaa ne yhteen edellisen ja seuraavan renderöinnin välillä riippumatta siitä, miten logiikkasi rakentuu.

Eri komponentit samassa sijainnissa nollaavat tilan

Tässä esimerkissä valintaruudun valitseminen korvaa <Counter>:n <p> tagilla:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Take a break
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Tässä vaihdat eri komponenttityyppejä samassa sijainnissa. Aluksi <div>:n ensimmäinen lapsi oli Counter. Mutta kun vaihdat <p>:ksi, React poistaa Countern UI puusta ja tuhoaa sen tilan.

Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin, jonka nimi on 'div' ja jonka yksi lapsi on 'Counter' ja joka sisältää tilan nimeltään 'count' arvolla 3. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentti on nyt poistettu, mikä on merkitty keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempi, mutta nyt sillä on uusi lapsi, jonka nimi on 'p' ja joka on korostettu keltaisella.
Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin, jonka nimi on 'div' ja jonka yksi lapsi on 'Counter' ja joka sisältää tilan nimeltään 'count' arvolla 3. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentti on nyt poistettu, mikä on merkitty keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempi, mutta nyt sillä on uusi lapsi, jonka nimi on 'p' ja joka on korostettu keltaisella.

Kun Counter vaihtuu p:ksi, Counter poistetaan ja p lisätään

Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin, jonka nimi on 'p'. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentti on nyt poistettu, mikä on merkitty keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempaa, mutta nyt siinä on uusi lapsikomponentti nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 0 ja joka on korostettu keltaisella.
Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin, jonka nimi on 'p'. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentti on nyt poistettu, mikä on merkitty keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempaa, mutta nyt siinä on uusi lapsikomponentti nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 0 ja joka on korostettu keltaisella.

Vaihdettaessa takaisin, p poistetaan ja Counter lisätään

Huomaathan, kun renderöit eri komponentin samassa sijainnissa, se nollaa sen koko alipuun tilan. Nähdäksesi, miten tämä toimii, lisää laskuri ja valitse sitten valintaruutu:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Laskurin tila nollautuu kun valintaruutu valitaan. Vaikka renderöit Counter:n, ensimmäinen <div>:n lapsi muuttuu <div>:stä <section>:ksi. Kun div:n lapsi poistettiin DOM:sta, sen koko alipuu (mukaan lukien Counter ja sen tila) tuhottiin.

Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin nimeltä 'div', jolla on yksi lapsi nimeltä 'section', jolla on yksi lapsi nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 3. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentit on nyt poistettu, mikä näkyy keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempaa, mutta nyt sillä on uusi lapsi 'div', joka on merkitty keltaisella ja jolla on myös uusi lapsi 'Counter', joka sisältää tilan 'count', jonka arvo on 0. Kaikki on merkitty keltaisella.
Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin nimeltä 'div', jolla on yksi lapsi nimeltä 'section', jolla on yksi lapsi nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 3. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentit on nyt poistettu, mikä näkyy keltaisella 'poof'-kuvalla. Kolmannessa osiossa on jälleen sama 'div'-vanhempaa, mutta nyt sillä on uusi lapsi 'div', joka on merkitty keltaisella ja jolla on myös uusi lapsi 'Counter', joka sisältää tilan 'count', jonka arvo on 0. Kaikki on merkitty keltaisella.

Kun section muuttuu div:ksi, section poistetaan ja uusi div lisätään

Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin nimeltä 'div', jolla on yksi lapsi nimeltä 'div', jolla on yksi lapsi nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 0. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentit on nyt poistettu, mikä osoitetaan keltaisella 'poof'-kuvalla. Kolmannella osiolla on jälleen sama 'div'-vanhempaa, mutta nyt sillä on uusi lapsi nimeltään 'section', joka on korostettu keltaisella ja jolla on myös uusi lapsi nimeltään 'Counter', joka sisältää tilan nimeltään 'count', jonka arvo on 0. Kaikki on korostettu keltaisella.
Kaavio, jossa on kolme osaa, joiden välissä on nuoli, joka yhdistää kunkin osan. Ensimmäinen osio sisältää React-komponentin nimeltä 'div', jolla on yksi lapsi nimeltä 'div', jolla on yksi lapsi nimeltä 'Counter', joka sisältää tilan nimeltä 'count', jonka arvo on 0. Keskimmäisessä osiossa on sama 'div'-vanhempi, mutta lapsikomponentit on nyt poistettu, mikä osoitetaan keltaisella 'poof'-kuvalla. Kolmannella osiolla on jälleen sama 'div'-vanhempaa, mutta nyt sillä on uusi lapsi nimeltään 'section', joka on korostettu keltaisella ja jolla on myös uusi lapsi nimeltään 'Counter', joka sisältää tilan nimeltään 'count', jonka arvo on 0. Kaikki on korostettu keltaisella.

Vaihdettaessa takaisin, div poistetaan ja uusi section lisätään

Nyrkkisääntönä, jos haluat säilyttää tilan renderöintien välillä, puun rakenteen on oltava “sama” renderöinnistä toiseen. Jos rakenteet ovat erilaiset, tila tuhotaan, sillä React tuhoaa tilan, kun se poistaa komponentin puusta.

Sudenkuoppa

Tämän takia sinun ei tulisi määritellä komponenttien sisällä komponentteja.

Tässä, MyTextField-komponenttia määritellään MyComponent-komponentin sisällä:

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

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

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

Joka kerta kun klikkaat painikkeesta, syötteen tila häviää! Tämä johtuu siitä, että eri MyTextField-funktio luodaan joka renderöinnille MyComponent:sta. Renderöit eri komponentin samassa sijainnissa, joten React nollaa kaiken tilan alapuolella. Tämä johtaa virheisiin ja suorituskykyongelmiin. Välttääksesi tämän ongelman, määrittele komponenttien funktiot aina ylemmällä tasolla, äläkä sisällytä niiden määrittelyjä.

Tilan nollaaminen samassa sijainnissa

Oletuksena React säilyttää komponentin tilan, kun se pysyy samassa sijainnissa. Yleensä tämä on juuri sitä, mitä haluat, joten se on järkevää oletusarvoa. Mutta joskus haluat nollata komponentin tilan. Harkitse tätä sovellusta, joka antaa kahdelle pelaajalle mahdollisuuden pitää kirjaa heidän pisteistään jokaisella vuorollaan:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Tommi" />
      ) : (
        <Counter person="Sara" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Seuraava pelaaja!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}n pisteet: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Lisää yksi
      </button>
    </div>
  );
}

Tällä hetkellä, kun vaihdat pelaajaa, tila säilyy. Kaksi Counter-komponenttia näkyy samassa sijainnissa, joten React näkee ne samana Counter-komponenttina, jonka person-prop on muuttunut.

Mutta tässä sovelluksessa niiden tulisi olla eri laskureita. Ne saattavat näkyä samassa kohdassa käyttöliittymää, mutta toinen laskuri on Tommin ja toinen laskuri Saran.

On kaksi tapaa nollata tila kun vaihdetaan niiden välillä:

  1. Renderöi komponentit eri sijainneissa
  2. Anna kullekin komponentille eksplisiittinen identiteetty key propilla

1. Vaihtoehto: Komponentin renderöiminen eri sijainneissa

Jos haluat, että nämä kaksi Counter-komponenttia ovat erillisiä, voit renderöidä ne eri sijainneissa:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Tommi" />
      }
      {!isPlayerA &&
        <Counter person="Sara" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Seuraava pelaaja!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}n pisteet: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Lisää yksi
      </button>
    </div>
  );
}

  • Aluksi, isPlayerA on arvoltaan true. Joten ensimmäinen sijainti sisältää Counter tilan ja toinen on tyhjä.
  • Kun klikkaat “Seuraava pelaaja” painiketta, ensimmäinen sijainti tyhjenee ja seuraava sisältää Counter:n.
Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'true'. Ainoa vasemmalle sijoitettu lapsi on nimeltään Counter, jonka tilakuplassa on merkintä 'count' ja arvo 0. Lapsi on korostettu keltaisella, mikä osoittaa, että se on lisätty.
Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'true'. Ainoa vasemmalle sijoitettu lapsi on nimeltään Counter, jonka tilakuplassa on merkintä 'count' ja arvo 0. Lapsi on korostettu keltaisella, mikä osoittaa, että se on lisätty.

Alkuperäinen tila

Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'false'. Tilakupla on korostettu keltaisella, mikä osoittaa, että se on muuttunut. Vasemmanpuoleinen lapsi on korvattu keltaisella 'poof'-kuvalla, joka osoittaa, että se on poistettu, ja oikealla on uusi lapsi, joka on korostettu keltaisella, mikä osoittaa, että se on lisätty. Uuden lapsen nimi on 'Counter' ja se sisältää tilakuplan 'count', jonka arvo on 0.
Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'false'. Tilakupla on korostettu keltaisella, mikä osoittaa, että se on muuttunut. Vasemmanpuoleinen lapsi on korvattu keltaisella 'poof'-kuvalla, joka osoittaa, että se on poistettu, ja oikealla on uusi lapsi, joka on korostettu keltaisella, mikä osoittaa, että se on lisätty. Uuden lapsen nimi on 'Counter' ja se sisältää tilakuplan 'count', jonka arvo on 0.

Painetaan “Seuraava pelaaja”

Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'true'. Tilakupla on korostettu keltaisella, mikä osoittaa, että se on muuttunut. Vasemmalla on uusi lapsi, joka on korostettu keltaisella, mikä osoittaa, että se on lisätty. Uuden lapsen nimi on 'Counter', ja siinä on tilakupla 'count', jonka arvo on 0. Oikeanpuoleinen lapsi on korvattu keltaisella 'poof'-kuvalla, joka osoittaa, että se on poistettu.
Kaavio, jossa on React-komponenttien puu. Vanhemman komponentin nimi on 'Scoreboard' ja sen tilakupla on isPlayerA, jonka arvo on 'true'. Tilakupla on korostettu keltaisella, mikä osoittaa, että se on muuttunut. Vasemmalla on uusi lapsi, joka on korostettu keltaisella, mikä osoittaa, että se on lisätty. Uuden lapsen nimi on 'Counter', ja siinä on tilakupla 'count', jonka arvo on 0. Oikeanpuoleinen lapsi on korvattu keltaisella 'poof'-kuvalla, joka osoittaa, että se on poistettu.

Painetaan “Seuraava pelaaja” uudelleen

Kunkin Counter:n tila tuhotaan joka kerta kun sen poistetaan DOM:sta. Tämän takia ne nollautuvat joka kerta kun painat painiketta.

Tämä ratkaisu on kätevä, kun sinulla on vain muutamia riippumattomia komponentteja, jotka renderöidään samassa paikassa. Tässä esimerkissä sinulla on vain kaksi, joten ei ole hankalaa renderöidä molemmat erikseen JSX:ssä.

2. Vaihtoehto: Tilan nollaaminen avaimella

On myös toinen, yleisempi tapa, jolla voit nollata komponentin tilan.

Olet saattanut nähdä key propseja kun renderöitiin listoja. Avaimet eivät ole vain listoille! Voit käyttää avaimia saadaksesi Reactin tunnistamaan erot komponenttien välillä. Oletuksena, React käyttää järjestystä (“ensimmäinen laskuri”, “toinen laskuri”) erottaakseen komponentit toisistaan. Mutta avaimilla voit kertoa Reactille, että tämä ei ole vain ensimmäinen laskuri ,tai toinen laskuri, vaan tarkemmin—esimerkiksi Tommin laskuri. Näin React tietää Tommin laskurin joka kerta kun näkee sen puussa!

Tässä esimerkissä, kaksi <Counter />:a eivät jaa tilaa vaikka ne näyttävät samassa paikassa JSX:ssä:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Tila säily vaikka Tommin ja Saran välillä vaihdetaan. Tämä tapahtuu koska annoit niille eri key:t.

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

Sen sijaan että komponentin sijainti tulisi järjestyksestä pääkomponentissa, key:n määrittäminen kertoo Reactille, että key itsessään on osa sijaintia. Tämän takia vaikka renderöit ne samassa sijainnissa JSX:ssä, Reactin perspektiivistä nämä ovat kaksi eri laskuria. Seurauksena, ne eivät koskaan jaa tilaa. Joka kerta kun laskuri näkyy ruudulla, sen tila on luotu. Joka kerta kun poistetaan, sen tila tuhotaan. Näiden välillä vaihtelu nollaa ja tuhoaa niiden tilan uudelleen ja uudelleen.

Huomaa

Muista, että avaimet eivät ole globaalisti uniikkeja. Ne määrittelevät sijainnin komponentin sisällä.

Lomakkeen nollaaminen avaimella

Tilan nollaaminen on erittäin hyödyllistä kun käsitellään lomakkeita.

Tässä chat-sovelluksessa, <Chat> komponentti sisältää tilan tekstisyötteelle:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Kokeile syöttää jotain kenttään ja painaa “Alice” tai “Bob” valitaksesi eri vastaanottajan. Huomaat, että syöte säilyy, koska <Chat> renderöidään samassa sijainnissa puussa.

Monissa sovelluksisa, tämä saattaa olla haluttu ominaisuus, mutta ei chat-sovelluksessa! Et halua käyttäjän lähettävän kirjoitettua viestiä väärälle henkilölle vanhingollisen klikkauksen seurauksena. Korjataksesi tämän, lisää key:

<Chat key={to.id} contact={to} />

Tämä varmistaa, että kun valitset eri vastaanottajan, Chat komponentti tullaan luomaan alusta, mukaan lukien tila puussa sen alla. React tulee myös luomaan uudelleen DOM elementit niiden uudelleenkäytön sijaan.

Nyt vaihtaminen vastaanottajien välillä tyhjää tekstisyötteen:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Syväsukellus

Tilan säilyttäminen poistetuille komponenteille

Oikeassa chat-sovelluksessa, haluaisit varmaankin palauttaa syötteen kun käyttäjä valitsee edellisen vastaanottajan uudestaan. On useita tapoja pitää tila “hengissä” komponentille, joka ei ole enää näkyvissä:

  • Voit renderöidä kaikki chatit yhden sijaan, ja piilottaa loput CSS:llä. Keskusteluja ei tultaisi poistamaan puusta, joten niiden paikallinen tila säilyisi. Tämä tapa toimii yksinkertaisille käyttöliittymille, mutta se voi koitua erittäin hitaaksi, jos piilotetut puut ovat suuria ja sisältävät paljon DOM nodeja.
  • Voit nostaa tilan ylös ja pitää viestiluonnokset jokaiselle vastaanottajalle pääkomponentissa. Tällä tavalla, kun lapsikomponentti poistetaan, se ei haittaa, sillä pääkomponentti pitää yllä tärkeät tiedot. Tämä on yleisin ratkaisu.
  • Saatat myös käyttää eri lähdettä Reactin tilan lisäksi. Esimerkiksi, saatat haluta viestiluonnoksen säilyvän vaikka käyttäjä vahingossa sulkisi sivun. Tämän tehdäksesi, voisit asettaa Chat komponentin alustamaan tilan lukemalla localStorage:a ja tallentamaan myös luonnokset sinne.

Riippumatta siitä minkä strategian valitset, chatti Alicen kanssa on havainnollisesti eri kuin Bobin kanssa, joten on järkevää antaa <Chat> komponentille key propsi, joka pohjautuu nykyiseen vastaanottajaan.

Kertaus

  • React pitää tilan niin pitkään kuin sama komponentti on renderöity samassa sijainnissa.
  • Tilaa ei pidetä JSX tageissa. Se liittyy puun sijaintiin, johon laitat JSX:n.
  • Voit pakottaa alipuun nollaamaan tilansa antamalla sille toisen avaimen.
  • Älä sijoita komponenttimäärityksiä toistensa sisään, tai nollaat tilan vahingossa.

Haaste 1 / 5:
Korjaa katoava syöttöteksti

Tämä esimerkki näyttää vistin kun painat painiketta. Kuitenkin painikkeen painaminen vahingossa nollaa syötteen. Miksi näin tapahtuu? Korjaa se, jotta painikkeen painaminen ei nollaa tekstisyötettä.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Hint: Your favorite city?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Hide hint</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Show hint</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}