Zrobimy coś, co może wydawać się proste, ale gwarantuję, że gdyby tak pokazać efekt końcowy i kazać odtworzyć, to początkujący polegnie, nawet jeśli wydaje mu się, że wszystko rozumie.
Ok, coś, czego absolutnie nie będziemy tłumaczyć, funkcja losująca:
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export {getRandomInt};
Teraz komponent timer:
import { useState, useEffect } from 'react';
export default function Timer({ timeout, onTimeout}) {
const [remainingTime, setRemainingTime] = useState(timeout);
useEffect(() => {
const timer = setTimeout(onTimeout, timeout);
return () => {
clearTimeout(timer);
};
}, [timeout, onTimeout]);
useEffect(() => {
const interval = setInterval(() => {
setRemainingTime((prevRemainingTime) => prevRemainingTime - 100);
}, 100);
return () => {
clearInterval(interval);
};
}, []);
return (
<progress
id="timer"
max={timeout}
value={remainingTime}
/>
);
}
Mamy tutaj dwa efekty, jeden na initial render, drugi na zmianę timeout bądź onTimeout, czyli propsów. Pierwsze to liczba, drugie to funkcja. Mamy też stan, jakim jest pozostały czas.
Teraz tak, pierwszy efekt kolejkuje wykonanie funkcji onTimeout po określonym czasie. Dodam, że w trakcie trwania timera możemy zmienić timeout lub ontimeout gdzieś wyżej i przekazać jako props.
Niżej mamy drugi efekt, który po prostu co 100ms wykonuje interwał, który zmniejsza remainingTime, czyli to, co pokazujemy jako value progressa. Bo max to timeout, zaś ten cały stan jest tylko po to, aby zmniejszać odpowiednio.
Cleanupy mam nadzieję, że ogarniamy. I raczej powinniśmy widzieć, co się tu dzieje. Co najwyżej lampka powinna nam się zapalić, że mamy funkcję w tablicy zależności useEffect. Tak, dobrze – taka funkcja to jest obiekt w JS. Typ referencyjny. To znaczy, że pewnie potrzebny będzie useCallback, inaczej ta funkcja nawet z niezmienioną definicją będzie się co render RFC swojego pojawiać z nową referencją (no chyba, że będzie powyżej poziomu RFC, ale takie funkcje z setState np. nie mogą korzystać).
Ok, dobra. Zobaczmy, co mamy w RFC głównym, czyli App:
import { getRandomInt } from "./utils";
import './App.css';
import { MainComponent } from './MainComponent.js';
import { useState, useCallback } from "react";
function App() {
let [randNum, setRandNum] = useState(() => getRandomInt(1,6));
let [timerKey, setTimerKey] = useState(1);
const handleClick = useCallback(function handleClick(){
let roll = getRandomInt(1,6);
setRandNum(roll);
setTimerKey((prevK) => prevK + 1);
}, []);
return (
<div className="App">
<MainComponent randNum={randNum} onTimeout={handleClick} timerKey={timerKey}></MainComponent>
</div>
);
}
export default App;
Mamy tu kilka rzeczy. Po pierwsze prosty stna globalny, randNum i setRandNum. Po drugie klucz, key. Po trzecie, funkcja handleClick. Ok, powiedzmy to szczerze – ona nie będzie się zmieniać nigdy. Więc niby czemu nie zabezpieczyć jej referencji wynosząc ją powyżej RFC?
No kuszące, ale wtedy nie użyjemy setRandNum i setTimerKey. Z kolei wewnątrz RFC App ta funkcja będzie łapać nową referencję co tick mówiąc w mega uproszczeniu. Nie chcemy tego, po to useCallback.
Jest takie ogólne, mylne przeświadczenie wannabe juniorów, że useCallback to „jakieś dynamiczne programowanie, jakaś silnia rekurencja”, bla bla bla. Nie. Ten hook zabezpiecza referencję danej funkcji. Aby co tick nie miała nowej, nową dostanie tylko wtedy, gdy de facto zmieni się coś w jej definicji (stąd tablica zależności w useCallback).
Ok, fajnie, a co ona robi? Losuje, ustawia randNum i ustawia nowy klucz updater funkcją. Zauważmy, że tablica zależności jest pusta dzięki temu i o to chodzi.
Ok, i teraz randNum, onTimeout i timerKey przekazujemy do MainComponent. To zobaczmy, co tam jest:
import Timer from "./Timer";
function MainComponent({randNum, onTimeout, timerKey}){
let timeout = 500;
return (
<>
<Timer timeout={timeout} onTimeout={onTimeout} key={timerKey} />
<p>Main Component: {randNum}</p>
<button onClick={onTimeout}>Randomize!</button>
</>
);
};
export { MainComponent };
Generalnie cała zabawa polega na tym:
- Czekamy czas minie czas
- Wtedy zostanie wylosowana i wyświetlona liczba
- Timer odpala się na nowo i zmniejsza się
- Możemy kliknąć guzik i wtedy od razu się wylosuje i timer przeskoczy dalej
Niby proste, niby banalne, ale serio… Ani proste, ani banalne. Jak ktoś uważa za banalne to mam dla niego takie wyzwanie:
<Timer timeout={timeout} onTimeout={onTimeout} key={1} /> //albo usuń key
Odpowiedz na pytanie, dlaczego ten key jest potrzebny. Dlaczego losowanie dalej działa (na klik) bez key, ale timer już nie. Drugie wyzwanie – dlaczego potrzebujemy useCallback. I nie wmawiaj sobie, że useCallback to jakieś algorytmy i inne rzeczy.
Hook useCallback jest od tego, że definiowanie jakiejś funkcji może zajmować dużo czasu. I można tego uniknąć co tick, zapamiętując jej definicję. Siłą rzeczy, referencja jest wtedy też taka sama, nie zmienia się co tick, a to pozwala nam wtedy takiej funkcji używać, przekazywać w dół bez strachu, że gdzieś będzie w tablicy zależności i nieskończoną pętlę wywoła.
Więcej Reacta niedługo!