Dłoń trzymająca pióro nad kartką z listą zadań

Dynamiczna lista kryptowalut – Pierwszy projekt w ReactJS #5

Naszedł czas na zabranie się za rdzeń trackera kryptowalut. Jest nim nic innego jak wyświetlanie kursów setek kryptowalut. Do tej pory samodzielnie wyświetlaliśmy każdą instancję komponentu Coin. Takie rozwiązanie ma kilka oczywistych wad. Narusza zasadę DRY, jest podatne na błędy przy referowaniu do kolejnego wycinka state, i przede wszystkim, kompletnie się nie skaluje. Musi być lepsze rozwiązanie. Jest nim połączenie możliwości programowania funkcyjnego oraz JSX.

Mapujemy kryptowaluty

Naszym wybawicielem będzie funkcja Array.prototype.map(). W świecie Reacta pozwala na stworzenie listy komponentów, którą zagnieżdża się w zwracanym JSX.

Odpowiedzialność za zautomatyzowanie wyświetlania Coin spocznie na barkach komponentu funkcyjnego CoinList.

// CoinList.js	

const CoinList = ({cryptos}) => {
  return cryptos.map(crypto =>  <Coin {...crypto} /> )
};

CoinList.propTypes = {
  cryptos: PropTypes.array,
}

To wszystko. Zbyt proste, żeby było prawdziwe? A jednak. CoinList otrzymuje tablicę kryptowalut od swojego rodzica za pośrednictwem props. Póki co, są to dane wprowadzone na sztywno do state. W przyszłości pobierzemy je z API CoinMarketCap. Z punktu widzenia CoinList, źródło danych jest kompletnie nieistotne. Poradzi sobie z ich wyświetlaniem bez konieczności wprowadzenia jakichkolwiek poprawek.

Czas na porządki w App.js. Bez skrupułów pozbywamy się czterech instancji Coin. Ich miejsce zastępuje CoinList.

// App.js
render() {
  return (
    <div>
      <Header cap={this.state.marketCap} />
      <CoinList cryptos={this.state.cryptos} />
    </div>
  );
}

Prezentuje się to znacznie lepiej. Zobaczmy, czy wszystko działa zgodnie z planem.

Lista wygląda jak należy, ale konsola wyrzuca błąd:

Błąd informujący o braku atrybutów key

Atrybut key

W Coinach zwracanych przez map zabrakło key. To specjalny atrybut służący do rozpoznawania elementów listy w Virtual DOM. React nie traci zasobów na głębokie porównywanie zawartości listy przy każdym rerenderze. Zadowala się porównywaniem key pomiędzy oryginalnym i nowym drzewem Virtual DOM. Taka heurystyka jest niesamowicie wydajna, ale wiąże się z kilkoma haczykami.

Warto wiedzieć: Klucze służą jedynie jako wskazówka dla algorytmu Reacta. Nie są przekazywane do komponentu jako props. Jeżeli chcesz uzyskać dostęp do wartości wykorzystanej jako key, powiel ją w propsach.

Zobaczmy, co stanie się przy próbie uciszenia Reacta najprostszym hackiem.

const CoinList = ({cryptos}) => {
  return cryptos.map(crypto =>  <Coin {...crypto} key="Panie, daj Pan spokój"/> )
};

Nic z tego. React odwdzięcza się kolejnym błędem:

Błąd informujący o zduplikowanym atrybucie key

Z deszczu pod rynnę. Co prawda wszystko działa zgodnie z oczekiwaniami, ale React ostrzega, że jesteśmy narażeni na duplikowanie i pominięcie części elementów.

Aby ustrzec się przed błędami, musimy spełnić podstawowy warunek. Każdy klucz musi mieć stałą, unikalną wartość. Ten wymóg ma lokalny charakter, dotyczy wyłącznie zakresu generowanej listy. Klucze w różnych listach mogą się powtarzać.

Indeksy jako klucze

Pierwsze, co przyszło mi na myśl, to wykorzystanie indeksów mapowanej tablicy.

const CoinList = ({cryptos}) => {
  return cryptos.map((crypto, i) =>  <Coin {...crypto} key={i} /> )
}	

Błędy wyparowały, ale nie chwalmy dnia przed zachodem słońca. Indeksy spełniają tylko jeden z dwóch kryteriów bezpiecznego klucza. Fakt faktem, są unikalne. Niestety, różnie to bywa z ich stałością.

Jeżeli na koniec cryptos trafi nowy obiekt, wszystko pójdzie zgodnie z planem.

Zasymuluję taki scenariusz w metodzie cyklu życia componentDidMount komponentu App. Więcej na temat metod cyklu życia w nadchodzącym wpisie.

componentDidMount() {
  this.setState({
    cryptos: [
        ...this.state.cryptos,
        { name: "Litecoin", acronym: "LTC", value: 141, cap: 492600000 }
    ]
  });
}

Teraz przeanalizujmy, jak wyglądają drzewa DOM przed i po wywołaniu setState.

// prevState 
<Coin key="1" name="Bitcoin" acronym="BTC" value={8714} cap={147379083734} />
<Coin key="2" name="Etherum" acronym="ETH" value={688} cap={67585640793} />
<Coin key="3" name="NEO" acronym="NEO" value={84} cap={5515789500} />
<Coin key="4" name="EOS" acronym="EOS" value={5} cap={4141934598} />

// nextState
<Coin key="1" name="Bitcoin" acronym="BTC" value={8714} cap={147379083734} />
<Coin key="2" name="Etherum" acronym="ETH" value={688} cap={67585640793} />
<Coin key="3" name="NEO" acronym="NEO" value={84} cap={5515789500} />
<Coin key="4" name="EOS" acronym="EOS" value={5} cap={4141934598} />
<Coin key="5" name="Litecoin" acronym="LTC" value={141} cap={492600000} />

React dopasował cztery pierwsze Coiny, a następnie dorzucił nowy na koniec drzewa. Obyło się bez zbędnego wysiłku.

Nie będzie tak różowo, zarówno dla mojego portfela, jak i trackera, jeżeli dojdzie do przewrotu na rynku. Wyobraźmy sobie, że tron Bitcoina zajmuje nowa waluta, a ja jestem zmuszony do dodania nowego obiektu na początek tablicy.

// prevState 
<Coin key="1" name="Bitcoin" acronym="BTC" value={8714} cap={147379083734} />
<Coin key="2" name="Etherum" acronym="ETH" value={688} cap={67585640793} />
<Coin key="3" name="NEO" acronym="NEO" value={84} cap={5515789500} />
<Coin key="4" name="EOS" acronym="EOS" value={5} cap={4141934598} />

// nextState
<Coin key="1" name="Litecoin" acronym="LTC" value={141} cap={492600000} />
<Coin key="2" name="Bitcoin" acronym="BTC" value={8714} cap={147379083734} />
<Coin key="3" name="Etherum" acronym="ETH" value={688} cap={67585640793} />
<Coin key="4" name="NEO" acronym="NEO" value={84} cap={5515789500} />
<Coin key="5" name="EOS" acronym="EOS" value={5} cap={4141934598} />

Heurystyka porównująca klucze zamiast zawartości właśnie zwróciła się przeciwko nam. Każda instancja otrzymała nowy klucz. Brak dopasowania pomiędzy atrybutami doprawdził do rerenderu całej listy.

Taka sytuacja udowadnia, że indeksy jako klucze są rozwiązaniem, któremu daleko do doskonałości. W 99% sytuacji mamy lepsze alternatywy. Często są nimi unikalne id przypisane do obiektu w bazie danych. Często, nie znaczy zawsze, czego tracker jest najlepszym przykładem. Na szczęście Coin ma dwie inne właściwości, które świetnie sprawdzą się w roli stałego, unikalnego klucza. To nameoraz acronym.

Indeksy jako klucze są bezpieczne tylko wtedy, jeżeli lista spełnia następujące kryteria:

  1. jest statyczna, nie ulega żadnym zmianom
  2. nie będzie sortowana ani filtrowana

Jeżeli Twoja lista zalicza się do pechowego 1%, powinieneś skorzystać z biblioteki do generowania hashy.

Podsumowanie

Tym razem przybliżyłem Ci działanie list komponentów i niebezpieczeństwa związane z niewłaściwymi keyami. Dwa następne wpisy będą dotyczyły wyświetlania warunkowego i obsługi zdarzeń. Później zabierzemy się za CSS, trzeba dodać trackerowi trochę blasku :).

Kod źródłowy projektu jest dostępny tutaj.

Podobał Ci się dzisiejszy artykuł? Udostępnij go w Twoich ulubionych mediach społecznościowych. Może znajdzie się ktoś, dla kogo również będzie wartościowy.

Bądź na bieżąco z serią Pierwszy projekt w ReactJS. Wystarczy polubić fanpage AlgoSmart na fejsie, obserwować mój profil na Twitterze i regularnie odwiedzać portal Polski Front-end.

Do zobaczenia za tydzień :).

Zdjęcie tytułowe autorstwa: unsplash-logoGlenn Carstens-Peters

Marcin Czarkowski

Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o React.js, JavaScript oraz CSS.