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:
Atrybut key
W Coin
ach 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 props
ach.
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:
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 map
owanej 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 Coin
y, 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 name
oraz acronym
.
Indeksy jako klucze są bezpieczne tylko wtedy, jeżeli lista spełnia następujące kryteria:
- jest statyczna, nie ulega żadnym zmianom
- nie będzie sortowana ani filtrowana
Jeżeli Twoja lista zalicza się do pechowego 1%, powinieneś skorzystać z biblioteki do generowania hashy.