Kontynuacja lekcji poprzednich, poznajemy bardzo dobrze zagadnienia renderowania i rerenderowania komponentów React. Do dzieła.
Z Githuba academind, z projektu Quiz, który już sobie omawialiśmy:
import { useRef } from 'react';
export default function Answers({
answers,
selectedAnswer,
answerState,
onSelect,
}) {
const shuffledAnswers = useRef();
if (!shuffledAnswers.current) {
shuffledAnswers.current = [...answers];
shuffledAnswers.current.sort(() => Math.random() - 0.5);
}
return (
<ul id="answers">
//(...)
</ul>
);
}
Mamy tu taką sytuację:
- RFC dostaje propsy takie jak answers, selectedAnswer, answerState, onSelect
- Prop answers nie będzie się zmieniać wewnątrz cyklu życia mount-unmount
- Reszta będzie się zmieniać dynamicznie, wybrał jakąś odpowiedź, zmienił się jej stan, chcemy zmienić CSS i tak dalej
- Chcemy na mount render pomieszać kolejność odpowiedzi, ale nie chcemy na każdy diff-render (zmieniający kolor bo zaznaczył odpowiedź albo wyświetliło mu się czy dobra czy zła) wywoływać pomieszania ponownie
I czym jest to pomieszanie? Czy wykonujemy jakiś efekt? Nie! Mieszamy stałą na poziomie RFC, która ma zostać pomieszana raz i koniec.
I do tego świetnie nadaje się useRef, któe tutaj odpali się na pierwszym renderem, później już warunek będzie nieprawdziwy i pod current będą już pomieszane odpowiedzi i dalej ten efekt nam działać nie będzie przy każdej zmianie koloru.
Ok, reszta komponentu:
return (
<ul id="answers">
{shuffledAnswers.current.map((answer) => {
const isSelected = selectedAnswer === answer;
let cssClass = '';
if (answerState === 'answered' && isSelected) {
cssClass = 'selected';
}
if (
(answerState === 'correct' || answerState === 'wrong') &&
isSelected
) {
cssClass = answerState;
}
return (
<li key={answer} className="answer">
<button
onClick={() => onSelect(answer)}
className={cssClass}
disabled={answerState !== ''}
>
{answer}
</button>
</li>
);
})}
</ul>
);
}
Mamy tutaj list-rendering, mamy binding atrybutu disabled, mamy trochę stylowania, wreszcie list rendering z map, które wewnątrz swojej funkcji może mieć ify (normalnie returny jsxowe nie mogą, mogą być ify na poziomie RFC z różnymi returnami JSXowymi).
Ale zauważmy, że tam mamy po prostu interpolację i zwykłą funkcję map, która przyjmuje funkcję i zwraca JSX reprezentujący jeden element, to takie v-for jakby ktoś Vue ogarniał, ale lepsze, bardziej sensowne moim zdaniem.
I tam mamy atrybut key, który w listach służy do czegoś innego. Wiemy, do czego służy key – jak zmieni się key, to następuje unmout komponentu, mount i mount render (nie diff render).
Mamy przykład z dokumentacji reacta z dokumentacji setState:
import { useState } from 'react';
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
Czyli tak:
- Jak jest mount render to initial state jest ustawiany
- Jak ktoś wywołuje setState to jest diff render
- Jak ktoś ustawi key, to zmiana tego key wywoła re-mount (unmount, mount, mount render)
- Skoro tak, można tego używać do resetowania komponentu, wystarczy mu key zmienić
I już z tego key tak korzystaliśmy, natomiast w przypadku list jest inaczej, aby React ogarniał coś takiego jak usunięcie elementu z listy (niekoniecznie ostatniego plus poruszam się tu pewnym skrótem myślowym, lista jest niemutowalna, po prostu jest re-render z nową listą o innej zawartości, jakiś tam element odfiltrowany) to aby React ogarnął to musi być key.
Key dla list musi być:
- Unikalne na poziomie rodzeństwa
- Niezmienne
Niezmienne to znaczy nie mamy w liście obiektów możliwości manipulacji ich id (w liście prostej indeks jest key). Unikalne na poziomie rodzeństwa znaczy dwie rzeczy, pierwsza nie utworzymy obiektów o takim samym id. Druga to należy pamiętać, że dwa komponenty obok siebie nie mogą mieć tej samej zmiennej jako klucza.
Ta druga rzecz się bardziej przydaje, gdy chcemy używać key do re-mountowania komponentu, natomiast w renderowaniu list po prostu musimy pamiętać, że lista obiektów musi mieć key, ten key to id property obiektu, które musi być unikalne.
Jeżeli nie usuwamy elementów z listy to nie ma problemu, natomiast gdy usuwamy, to brak key powoduje różne dziwne bugi. Coś z usuniętego elementu, z jego stanu, wskakuje do stanu innego elementu, który nie był ruszony i tak dalej.
Też jest tak, że React robi diffing i generalnie jak mamy re-render bo się stan zmienił bo jest nowa tablica, z nowym elementem odfiltrowanym, to koniec końców na poziomie DOMu React usuwa sobie ostatni element li. A w kontekście tablicy usunęliśmy element środkowy.
I zaczynają się dziać „dziwne rzeczy”. A tak key jak się zmieni to jest re-mount. Co sprawia, że nieważne co React „myśli” sobie po diffingu, usuwanie z listy nie powoduje tych problemów.
Tak mi się przynajmniej wydaje, może nie mam racji i key w listach i w komponentach, które można resetować to zupełnie dwa różne pojęcia, koniec końców pamiętać musimy o tym:
- Nie zresetujesz komponentu (nie zrobisz mu unmount, mount, mount render) jeżeli nie dasz mu key, który można zmienić – ten key może się zmieniać, ma się zmieniać, ale ta sama zmienna ma nie być key jakiegoś jego rodzeństwa
- Nie wyświetlisz poprawnie listy itemów, jeżeli nie dasz im unikalnego atrybutu key, bo będą błędy, gdy zaczniesz usuwać z listy – to key ma być unikalne, unikalne dla każdego tagu w liście i tak samo unikalne dla każdego elementu w tablicy, która jest stanem, mapujące element tablicy do elementu listy i te keys nie mają się zmieniać, bo mapowanie popsują