Čuvanje i resetovanje state-a

State je izolovan između komponenata. React prati koji state pripada kojoj komponenti na osnovu njenog mesta u UI stablu. Možete kontrolisati kada čuvati state, a kada ga resetovati između ponovnih rendera.

Naučićete:

  • Kada React bira da li da čuva ili resetuje state
  • Kako naterati React da resetuje state komponente
  • Kako ključevi i tipovi utiču na to da li je state sačuvan

State je povezan sa pozicijom u stablu renderovanja

React pravi stabla renderovanja za strukturu komponenti na vašem UI-u.

Kada komponenti date state, možete pomisliti da state “živi” unutar komponente. Ali, state se zapravo čuva unutar React-a. React povezuje svaki deo state-a sa tačnom komponentom na osnovu mesta na kom se ta komponenta nalazi u stablu renderovanja.

Ovde postoji samo jedan <Counter /> JSX tag, ali je renderovan na dve različite pozicije:

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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Evo kako to izgleda u obliku stabla:

Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Svako dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0.
Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Svako dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0.

React stablo

Ovo su dva odvojena brojača jer je svaki renderovan na svojoj poziciji u stablu. Obično ne morate razmišljati o ovim pozicijama da biste koristili React, ali može biti korisno da razumete kako to funkcioniše.

U React-u, svaka komponenta na ekranu ima potpuno izolovan state. Na primer, ako renderujete dve Counter komponente jednu pored druge, svaka će imati svoje nezavisne score i hover state-ove.

Probajte da kliknete oba brojača i primetite da ne utiču jedan na drugog:

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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Kao što možete videti, kada je jedan brojač ažuriran, jedino se state za tu komponentu ažurira:

Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Desno dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 1. State balon desnog deteta je istaknut žutom bojom da bi se naznačilo da mu je vrednost ažurirana.
Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Desno dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 1. State balon desnog deteta je istaknut žutom bojom da bi se naznačilo da mu je vrednost ažurirana.

Ažuriranje state-a

React će držati state dok god istu komponentu renderujete na istoj poziciji u stablu. Da biste ovo primetili, inkrementirajte oba brojača, uklonite drugu komponentu tako što ćete odštiklirati checkbox “Renderuj drugi brojač”, a onda je štikliranjem dodajte ponovo:

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)
          }}
        />
        Renderuj drugi brojač
      </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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Primetite da u trenutku kada prestanete da renderujete drugi brojač, njegov state potpuno nestaje. To je zato što kad React ukloni komponentu, uklanja i njen state.

Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Desno dete nedostaje i na njegovom mestu je žuta 'poof' slika, ističući da se komponenta briše iz stabla.
Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Desno dete nedostaje i na njegovom mestu je žuta 'poof' slika, ističući da se komponenta briše iz stabla.

Brisanje komponente

Kada štiklirate “Renderuj drugi brojač”, drugi Counter i njegov state se inicijalizuju od nule (score = 0) i dodaju u DOM.

Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Desno dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Celokupan čvor desnog deteta je istaknut žutom bojom, označavajući da je upravo dodat u stablo.
Dijagram stabla React komponenata. Korenski čvor nazvan 'div' ima dva deteta. Levo dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Desno dete se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošcu 0. Celokupan čvor desnog deteta je istaknut žutom bojom, označavajući da je upravo dodat u stablo.

Dodavanje komponente

React čuva state komponente dok god se renderuje na svojoj poziciji u UI stablu. Ako se ukloni, ili se druga komponenta renderuje na istoj poziciji, React odbacuje njen state.

Ista komponenta na istoj poziciji čuva state

U ovom primeru postoje dva različita <Counter /> tag-a:

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)
          }}
        />
        Koristi fensi stajling
      </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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Kada promenite checkbox, counter state ne bude resetovan. Nezavisno od toga da li je isFancy jednak true ili false, uvek ćete imati <Counter /> kao prvo dete div-a koje je vraćeno iz korenske App komponente:

Dijagram sa dva dela razdvojena strelicom koja prelazi od jednog ka drugom. Svaki deo sadrži raspored komponenata sa roditeljem pod imenom 'App' koji sadrži state balon nazvan isFancy. Ova komponenta ima jedno dete nazvano 'div' koje vodi do prop balona koji sadrži isFancy (istaknuto ljubičastom bojom) i prosleđuje se jedinom detetu. Poslednje dete pod imenom 'Counter' sadrži state balon sa nazivom 'count' i vrednošcu 3 u oba dijagrama. U levom delu dijagrama ništa nije istaknuto i vrednost roditeljskog isFancy state-a je false. U desnom delu dijagrama vrednost roditeljskog isFancy state-a se promenila na true i istaknuta je žutom bojom, kao i prop balon ispod, čija isFancy vrednost se takođe promenila na true.
Dijagram sa dva dela razdvojena strelicom koja prelazi od jednog ka drugom. Svaki deo sadrži raspored komponenata sa roditeljem pod imenom 'App' koji sadrži state balon nazvan isFancy. Ova komponenta ima jedno dete nazvano 'div' koje vodi do prop balona koji sadrži isFancy (istaknuto ljubičastom bojom) i prosleđuje se jedinom detetu. Poslednje dete pod imenom 'Counter' sadrži state balon sa nazivom 'count' i vrednošcu 3 u oba dijagrama. U levom delu dijagrama ništa nije istaknuto i vrednost roditeljskog isFancy state-a je false. U desnom delu dijagrama vrednost roditeljskog isFancy state-a se promenila na true i istaknuta je žutom bojom, kao i prop balon ispod, čija isFancy vrednost se takođe promenila na true.

Ažuriranje App state-a ne resetuje Counter zato što Counter ostaje na istoj poziciji

To je ista komponenta na istoj poziciji, pa je iz React perspektive to isti brojač.

Pitfall

Zapamtite da je pozicija u UI stablu—ne u JSX markup-u—ono što je React-u bitno! Ova komponenta ima dva return iskaza sa različitim <Counter /> JSX tag-ovima unutar i izvan if-a:

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)
            }}
          />
          Koristi fensi stajling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Koristi fensi stajling
      </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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Možda biste očekivali da se state resetuje kada promenite checkbox, ali to nije slučaj! To se dešava jer su oba <Counter /> tag-a renderovana na istoj poziciji. React ne zna gde vi pravite uslove u vašoj funkciji. Sve što “vidi” je stablo koje vratite.

U oba slučaja, App komponenta vraća <div> sa <Counter /> kao prvim detetom. Za React, ova dva brojača imaju iste “adrese”: prvo dete prvog deteta od korena. Ovako ih React poredi između prethodnih i narednih rendera, nezavisno od strukture vaše logike.

Različite komponente na istoj poziciji resetuju state

U ovom primeru, štikliranje checkbox-a će zameniti <Counter> sa <p>:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>Vidimo se posle!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Uzmi pauzu
      </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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Ovde menjate između različitih tipova komponenti na istoj poziciji. Inicijalno, prvo dete <div>-a je bio Counter. Ali kad ste ga zamenili sa p, React je uklonio Counter iz UI stabla i uništio njegov state.

Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 3. Srednji deo ima istog 'div' roditelja, ali je dečja komponenta sada obrisana, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'p', koji je istaknut žutom.
Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 3. Srednji deo ima istog 'div' roditelja, ali je dečja komponenta sada obrisana, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'p', koji je istaknut žutom.

Kada se Counter zameni sa p, Counter je obrisan i p je dodat

Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'p'. Srednji deo ima istog 'div' roditelja, ali je dečja komponenta sada obrisana, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0 i istaknut je žutom.
Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'p'. Srednji deo ima istog 'div' roditelja, ali je dečja komponenta sada obrisana, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0 i istaknut je žutom.

Kada se ponovo promene, p je obrisan, a Counter je dodat

Takođe, kada renderujete različitu komponentu na istoj poziciji, resetuje se state od čitavog podstabla. Da vidite kako ovo radi, inkrementirajte brojač i onda štiklirajte checkbox:

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)
          }}
        />
        Koristi fensi stajling
      </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)}>
        Dodaj jedan
      </button>
    </div>
  );
}

State Counter-a se resetuje kada kliknete na checkbox. Iako renderujete Counter, prvo dete div-a se promeni sa section na div. Kada je dečji section uklonjen iz DOM-a, celokupno stablo ispod (uključujući Counter i njegov state) je takođe uništeno.

Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'section', koji ima jedno dete sa nazivom 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 3. Srednji deo ima istog 'div' roditelja, ali su dečje komponente sada obrisane, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'div' i istaknutog žutom, takođe sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0, sve istaknuto žutom.
Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'section', koji ima jedno dete sa nazivom 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 3. Srednji deo ima istog 'div' roditelja, ali su dečje komponente sada obrisane, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'div' i istaknutog žutom, takođe sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0, sve istaknuto žutom.

Kada se section promeni u div, section je obrisan, a novi div je dodat

Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'div', koji ima jedno dete sa nazivom 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Srednji deo ima istog 'div' roditelja, ali su dečje komponente sada obrisane, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'section' i istaknutog žutom, takođe sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0, sve istaknuto žutom.
Dijagram sa tri dela i strelicom koja prelazi između njih. Prvi deo sadrži React komponentu nazvanu 'div' sa jednim detetom nazvanim 'div', koji ima jedno dete sa nazivom 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Srednji deo ima istog 'div' roditelja, ali su dečje komponente sada obrisane, što je označeno žutom 'poof' slikom. Treći deo ponovo ima istog 'div' roditelja, ali sada sa novim detetom nazvanim 'section' i istaknutog žutom, takođe sa novim detetom nazvanim 'Counter' koji sadrži state balon sa nazivom 'count' i vrednošću 0, sve istaknuto žutom.

Kada se ponovo promene, div je obrisan, a novi section je dodat

Kao pravilo, ako želite da sačuvate state između ponovnih rendera, struktura vašeg stabla mora da se “poklapa” od jednog do drugog rendera. Ako je struktura različita, state će biti uništen jer React uništava state kada uklanja komponentu iz stabla.

Pitfall

Zbog ovoga ne biste trebali da ugnježdavate definicije funkcija komponenti.

Ovde je funkcija MyTextField komponente definisana unutar MyComponent:

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)
      }}>Kliknuto {counter} puta</button>
    </>
  );
}

Svaki put kad kliknete dugme, input state nestane! To se dešava jer je različita MyTextField funkcija kreirana za svaki render MyComponent-a. Renderujete različitu komponentu na istoj poziciji, pa React resetuje sve state-ove ispod. Ovo prouzrokuje bug-ove i probleme sa performansama. Da biste izbegli ovaj problem, uvek deklarišite funkcije komponenti na najvišem nivou i nemojte ugnježdavati njihove definicije.

Resetovanje state-a na istoj poziciji

Po default-u, React čuva state komponente dok god ostaje na istoj poziciji. Uglavnom je to upravo ono što želite, pa ima smisla da je to default ponašanje. Ali, ponekad možete želeti da resetujete state komponente. Razmotrite ovu aplikaciju koja omogućava dvojici igrača da prate svoje poene tokom svakog poteza:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Naredni igrač!
      </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>Poeni za osobu {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Trenutno, kada promenite igrača, poeni ostaju sačuvani. Dve Counter komponente se nalaze na istoj poziciji, pa ih React posmatra kao isti Counter čiji se person prop promenio.

Ali konceptualno, u ovoj aplikaciji to trebaju biti dva odvojena brojača. Iako se pojavljuju na istom mestu na UI-u, jedan brojač je za Taylor, a drugi za Sarah.

Postoje dva načina da resetujete state kada ih menjate:

  1. Renderujte komponente na različitim pozicijama
  2. Dajte svakoj komponenti eksplicitni identitet sa key

Opcija 1: Renderovanje komponente na različitim pozicijama

Ako želite da ova dva Counter-a budu nezavisna, možete ih renderovati na dve različite pozicije:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Naredni igrač!
      </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>Poeni za osobu {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Dodaj jedan
      </button>
    </div>
  );
}

  • Inicijalno, isPlayerA je true. To znači da prva pozicija sadrži Counter state, a druga je prazna.
  • Kada kliknete dugme “Naredni igrač” prva pozicija se briše, a druga sada sadrži Counter.
Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'true'. Jedino dete, raspoređeno levo, se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Celokupno levo dete je istaknuto žutom, označavajući da je dodato.
Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'true'. Jedino dete, raspoređeno levo, se naziva 'Counter' i sadrži state balon sa nazivom 'count' i vrednošću 0. Celokupno levo dete je istaknuto žutom, označavajući da je dodato.

Inicijalni state

Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'false'. State balon je istaknut žutom, označavajući da je promenjen. Levo dete je zamenjeno sa žutom 'poof' slikom označavajući da je obrisano, a tu je novo dete sa desne strane, istaknuto žutom, označavajući da je dodato. Novo dete sa nazivom 'Counter' sadrži state balon sa nazivom 'count' i vrednošću 0.
Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'false'. State balon je istaknut žutom, označavajući da je promenjen. Levo dete je zamenjeno sa žutom 'poof' slikom označavajući da je obrisano, a tu je novo dete sa desne strane, istaknuto žutom, označavajući da je dodato. Novo dete sa nazivom 'Counter' sadrži state balon sa nazivom 'count' i vrednošću 0.

Kliknuto “naredno”

Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'true'. State balon je istaknut žutom, označavajući da je promenjen. Postoji novo dete na levoj strani, istaknuto žuto, označavajući da je dodato. Novo dete sa nazivom 'Counter' sadrži state balon sa nazivom 'count' i vrednošću 0. Desno dete je zamenjeno sa žutom 'poof' slikom označavajući da je obrisano.
Dijagram sa stablom React komponenata. Roditelj se naziva 'Scoreboard' i sadrži state balon sa nazivom 'isPlayerA' i vrednošću 'true'. State balon je istaknut žutom, označavajući da je promenjen. Postoji novo dete na levoj strani, istaknuto žuto, označavajući da je dodato. Novo dete sa nazivom 'Counter' sadrži state balon sa nazivom 'count' i vrednošću 0. Desno dete je zamenjeno sa žutom 'poof' slikom označavajući da je obrisano.

Kliknuto “naredno” opet

State svakog Counter-a bude uništen svaki put kad je uklonjen iz DOM-a. Zbog toga se resetuju svaki put kada kliknete dugme.

Ovo rešenje je zgodno kada imate samo par komponenti renderovanih na istom mestu. U ovom primeru, imate ih samo dve, pa nije problem renderovati ih u različitom JSX-u.

Opcija 2: Resetovanje state-a uz key

Postoji i drugi, više generički, način da resetujete state komponente.

Možda ste videli key-eve tokom renderovanja listi. Ključevi nisu samo za liste! Možete koristiti ključeve da naterate React da razlikuje bilo koje komponente. Po default-u, React koristi redosled unutar roditelja (“prvi brojač”, “drugi brojač”) da razlikuje komponente. Ali, ključevi vam omogućavaju da kažete React-u da to nije samo prvi brojač, ili drugi brojač, već poseban brojač—na primer, Taylor-ov brojač. Na ovaj način, React će znati za Taylor-ov brojač gde god da se pojavi u stablu!

U ovom primeru, dva <Counter />-a ne dele state iako se nalaze na istom mestu u JSX-u:

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);
      }}>
        Naredni igrač!
      </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>Poeni za osobu {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Dodaj jedan
      </button>
    </div>
  );
}

Menjanje između Taylor i Sarah ne čuva state. To je zato što ste im dali različite key-eve:

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

Specificiranje key-a govori React-u da koristi key kao deo pozicije umesto rasporeda unutar roditelja. Zbog toga, iako ih renderujete na istom mestu u JSX-u, React ih vidi kao dva različita brojača, tako da nikad neće deliti state. Svaki put kad se brojač pojavi na ekranu, njegov state je kreiran. Svaki put kad je uklonjen, njegov state je uništen. Promena između njih resetuje njihov state iznova.

Napomena

Zapamtite da ključevi nisu jedinstveni globalno. Oni samo označavaju poziciju unutar roditelja.

Resetovanje forme sa key-em

Resetovanje state-a uz key je posebno korisno kada radite sa formama.

U ovoj aplikaciji za poruke, <Chat> komponenta sadrži state za tekstualni input:

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' }
];

Pokušajte da unesete nešto u input, a onda pritisnite “Alice” ili “Bob” da odaberete drugog primaoca. Primetićete da je input state sačuvan zato što je <Chat> renderovan na istoj poziciji u stablu.

U mnogim aplikacijama ovo može biti željeno ponašanje, ali ne i u aplikaciji za poruke! Ne želite da dopustite korisniku da pošalje već otkucanu poruku pogrešnoj osobi zbog slučajnog klika. Da biste ovo popravili, dodajte key:

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

Ovo osigurava da kada odaberete drugog primaoca, da će Chat komponenta biti napravljena od nule, uključujući bilo koji state u stablu ispod. React će takođe ponovo kreirati DOM elemente umesto da ih ponovo iskoristi.

Sada promena primaoca uvek briše tekstualno polje:

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' }
];

Deep Dive

Čuvanje state-a za uklonjene komponente

U pravoj aplikaciji za poruke, verovatno ćete želeti da povratite input state kada korisnik izabere prethodnog primaoca ponovo. Postoji par načina da držite state “živim” za komponentu koja nije više vidljiva:

  • Mogli biste da renderujete sve chat-ove umesto samo jednog, ali da ostale sakrijete pomoću CSS-a. Chat-ovi neće biti uklonjeni iz stabla, pa će njihov lokalni state biti sačuvan. Ovo rešenje radi dobro za jednostavne UI-e. Ali, može postati dosta sporo ako su skrivena stabla velika i sadrže mnogo DOM čvorova.
  • Mogli biste podići state i čuvati poruke na čekanju za svakog primaoca u roditeljskoj komponenti. Na ovaj način, nije bitno ako dečje komponente budu uklonjene, jer njihov roditelj čuva bitne informacije. Ovo je najčešće rešenje.
  • Takođe možete koristiti i druge izvore informacija, kao ispomoć React state-u. Na primer, verovatno želite da sačuvate draft poruku čak iako korisnik slučajno zatvori stranicu. Da biste to implementirali, mogli biste napraviti da Chat komponenta inicijalizuje svoj state čitanjem iz localStorage-a, a takođe i da čuva draft poruke tamo.

Nevezano za to koju strategiju odaberete, chat sa Alice je konceptualno drugačiji od chat-a sa Bob-om, tako da ima smisla zadati key u <Chat> stablo na osnovu trenutnog primaoca.

Recap

  • React čuva state dok god se ista komponenta renderuje na istoj poziciji.
  • State se ne čuva u JSX tag-ovima. Povezan je sa pozicijom u stablu na koju stavljate taj JSX.
  • Možete forsirati podstablo da resetuje svoj state tako što ćete mu dati drugačiji key.
  • Nemojte ugnježdavati definicije komponenti, ili ćete slučajno resetovati state.

Izazov 1 od 5:
Popraviti nestajući tekst u input-u

Ovaj primer prikazuje poruku kada kliknete dugme. Međutim, klik na dugme slučajno resetuje input. Zašto se ovo dešava? Popravite to tako da klik na dugme ne resetuje tekst u input-u.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Pomoć: Vaš omiljeni grad?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Sakrij pomoć</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Prikaži pomoć</button>
    </div>
  );
}

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