Poznajemy lepiej zagadnienia renderowania i re-renderowania komponentów React oraz jaki wpływ my mamy na to, co się dzieje. Do dzieła.
Ok, po pierwsze dokumentacja Reacta używa dość mylących moim zdaniem określeń:
- Initial render, wykonywany zaraz po mountcie komponentu, nazwiemy go sobie mount renderem
- Re-render, nazwiemy go sobie diff-renderem (od diffing), wykonywany gdy:
- Zmieni się stan komponentu
- Zmieni się kontekst komponentu
- Komponent-rodzic sam ma diff-render, na przykład wywołany jego zmianą stanu
Ok, do czego służy memo:
- Memo służy do tego, aby komponent traktowany jako dziecko nie wykonywał diff-rendera wtedy, gdy jego rodzic ma diff-render, ale propsy przekazywane do dziecka nie uległy zmianie
Ok, postarajmy się zrozumieć tablicę zależności useEffect:
- Brak tablicy zależności – efekt odpalany na mount render i każdy diff-render
- Tablica zależności pusta – efekt odpalany na mount render i koniec
- Tablica zależności – efekt odpalany na mount render i te diff-rendery, podczas których zależność uległa zmianie
Ok, postarajmy się to wbić do głowy – setState nie wywołuje re-mountu, tylko re-render (diff render)! Może wydawać się oczywiste, ale często takie nie jest.
Oznacza to, że jeżeli nasz useEffect ma pustą tablicę zależności, to zmiana stanu nie wywoła tego efektu. I co ważniejsze – zmiana stanu rodzica również nie wywoła tego efektu. Może się wydawać, że skoro rodzic zmienia stan to dzieci są re-mountowane – nie są.
Rodzic ma diff-render (zmienił się jego stan), jego dzieci mają diff-render. Jeżeli jego dzieci są wyposażone w memo to mają ten diff-render tylko wtedy, gdy przekazywane propsy do nich się zmieniają.
Academind ma na swoim GitHubie bardzo ciekawy komponent, który sobie omówimy:
import { useState, useEffect } from 'react';
export default function QuestionTimer({ timeout, onTimeout, mode }) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
console.log('SETTING TIMEOUT');
const timer = setTimeout(onTimeout, timeout);
return () => {
clearTimeout(timer);
};
}, [timeout, onTimeout]);
useEffect(() => {
console.log('SETTING INTERVAL');
const interval = setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<progress
id="question-time"
max={timeout}
value={remainingTime}
className={mode}
/>
);
}
Czyli tak:
- timeout, ontimeout i mode to propsy
- jego RFC nie korzysta z memo, czyli diff-render rodzica to diff-render dziecka
- pierwszy useEffect odpali się na mount-render i tyle, może się też odpalić na diff-rendery, gdyby zależności z propsów się zmieniły, choć tutaj widać, że nie mają być zmieniane co do konstrukcji
- mamy też cleanup, czyli przed unmountem lub diff-renderem, który by ten efekt odpalił, mamy clearTimeout
- drugi useEffect odpali się tylko na mount render.
- drugi useEffect ma pustą tablicę zależności, ponieważ do setState przekazano updater function
- przed unmountem odpali się cleanup, clearInterval
- raz odpalony interval będzie co 100ms odpalał setState
- setState wywoła, uwaga… diff render! Zatem useEffect z pustą tablicą zależności ma go gdzieś, tylko mount render go interesuje
Ok, rozbijemy sobie ten temat na kilka lekcji, bo to rzeczywiście może być trudne do zrozumienia.