Pierwszy rzut oka na useRef, bardzo przydatny hook Reacta, służący do tworzenia stanów, których zmiana nie wywołuje re-rendera oraz manipulacji drzewem DOM. Do dzieła.
Do useRef, importowanego jak większość hooków Reacta, używamy initial state:
import { useRef } from 'react';
function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
Zwracany jest obiekt, pod którym w current będzie znajdowała się wartość ref.
Kilka rzeczy, o których warto pamiętać:
- W odróżnieniu od useState, który zwracał state (wartość do odczytu) i setState (funkcję do zapisu), ref.current jest tak odczytywalny jak i mutowalny
- Nie powinniśmy mutować ref.current, jeżeli jego wartość jest wykorzystywana do renderowania
- Zmiana ref.current nie wywołuje renderowania jak zmiana stanu
- Nie powinniśmy odczytywać ani zapisywać do ref.current podczas renderowania, nie licząc inicjalizacji, bo komponent może zachowywać się w sposób nieprzewidywalny
Co daje nam useRef:
- Możesz trzymać tam informacje, które przetrwają re-renderowanie (w odróżnieniu od typowych zmiennych, które są resetowane pomiędzy re-renderami)
- Zmiana ref nie wywołuje re-renderowania (w odróżnieniu od stanu, który re-renderuje komponent przy każdej zmianie)
- Informacja jest lokalna dla każdej kopii komponentu (w odróżnieniu od zmiennych na zewnątrz, które są współdzielone)
Pierwszy przykład z dokumentacji Reacta – counter:
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
Tu mamy wyświetlanie ref dopiero po kliknięciu przycisku, w alercie, zatem renderowanie ponowne komponentu nie jest potrzebne. Dlatego użyto ref.
Kolejny przykład z docsów Reacta, warto mieć na uwadze, że czasem są one przesadnie uproszczone, aby nie wprowadzać innych zagadnień:
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
Ok, mamy useState i useRef. Zmienne stanowe startTime oraz now, zmienna ref intervalRef. Teraz funkcja start i stop:
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
Mamy setInterval, który wywołuje zmianę stanu zmiennej now, ale samo interval id znajduje się w zmiennej ref i re-rendera nie wywołuje np. funkcja handleStop.
Reszta komponentu:
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
I pytanie za sto punktów – co robi clearInterval w funkcji handleStart?
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
Cóż, pomyślmy:
- Tworzymy interwał, który co 10ms się wykonuje
- Ten interwał można zatrzymać funkcją stop
- Co 10ms zmienia się stan
- Co 10ms zachodzi re-render
- Czy zatem co 10ms tworzony jest nowy interwał, który trzeba czyścić?
Nie. I tak i nie. Funkcja handleStart tworzy interwał, który chodzi co 10ms. I jeżeli go nie zatrzymamy, to on tam w tej pętli zdarzeń jest. I zmienia stan, i zmiana stanu wywołuje re-render, ale samo handleStart jest odpalane za pomocą kliknięcia, więc jest cały czas jeden interwał, aż do kliknięcia stop (wtedy jest 0).
Natomiast istnieje teoretyczna możliwość, że ktoś będzie spamował guzik start wiele razy. Wtedy tworzyłby kolejne interwały, i tylko ich numer id byłby nadpisywany poprzez id ostatniego.
Można to inaczej rozwiązać, można zrobić disabled na start button, można sprawdzić, czy intervalRef.current jest nulem i tylko wtedy odpalać funkcję, ważne abyśmy przynajmniej mniej-więcej ogarniali, co się tutaj w Reakcie dzieje…
Ważna uwaga z dokumentacji – React oczekuje, że ciało funkcji naszego komponentu będzie zachowywać się jak czysta funkcja, czyli:
- Przy takich samych propsach, stanie i kontekście zawsze wynikiem będzie taki sam JSX
- Wywoływanie w różnej kolejności albo z różnymi argumentami ma nie wpływać na inne wywołania
Zaznaczają, że pisanie lub czytanie refów podczas renderowania niszczy tę regułę:
function MyComponent() {
// ...
// 🚩 Don't write a ref during rendering
myRef.current = 123;
// ...
// 🚩 Don't read a ref during rendering
return <h1>{myOtherRef.current}</h1>;
}
Pokazują, że do odczytywania refów są efekty, zaś od ich zmieniania – event handlery:
function MyComponent() {
// ...
useEffect(() => {
// ✅ You can read or write refs in effects
myRef.current = 123;
});
// ...
function handleClick() {
// ✅ You can read or write refs in event handlers
doSomething(myOtherRef.current);
}
// ...
}
Więcej Reacta niedługo…