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.