Lekcja 18: Hook useRef
Hook useRef
jest kolejnym podstawowym Hookiem w React, ale jego zastosowanie różni się od useState
, useEffect
czy useContext
. Głównym celem useRef
jest umożliwienie dostępu do węzłów DOM lub przechowywanie dowolnej mutowalnej wartości, która nie powoduje ponownego renderowania komponentu przy jej zmianie.
Czym jest `useRef`?
useRef
zwraca mutowalny obiekt referencji, którego właściwość .current
jest inicjalizowana przekazanym argumentem (initialValue
). Zwrócony obiekt będzie istniał przez cały cykl życia komponentu.
Składnia:**
import React, { useRef } from \"react\";
function MyComponent() {
const myRef = useRef(initialValue);
// Dostęp do wartości: myRef.current
// Modyfikacja wartości: myRef.current = newValue;
// ... reszta komponentu
}
Kluczowe cechy `useRef`:
- Mutowalność: Wartość przechowywana w
myRef.current
może być dowolnie modyfikowana. - Brak ponownego renderowania: Zmiana wartości
myRef.current
nie powoduje ponownego renderowania komponentu. To główna różnica w porównaniu do stanu zarządzanego przezuseState
. - Trwałość: Obiekt referencji (i wartość
.current
) jest zachowywany między kolejnymi renderowaniami komponentu.
Główne Zastosowania `useRef`
1. Dostęp do Węzłów DOM
Jest to najczęstsze zastosowanie useRef
. Pozwala uzyskać bezpośredni dostęp do elementu DOM renderowanego przez React, aby móc wywoływać na nim metody imperatywne (np. focus()
, blur()
, pomiar rozmiaru, integracja z bibliotekami non-React).
Jak to działa:
- Stwórz referencję za pomocą
useRef(null)
. - Przekaż tę referencję do atrybutu
ref
elementu JSX, do którego chcesz uzyskać dostęp:<input ref={myInputRef} />
. - Gdy React renderuje element DOM, przypisze referencję do tego węzła DOM do właściwości
.current
obiektu ref (myInputRef.current
będzie teraz wskazywać na element<input>
w DOM). - Możesz teraz uzyskać dostęp do węzła DOM przez
myInputRef.current
, ale zazwyczaj robisz to wewnątrzuseEffect
lub handlerów zdarzeń, aby mieć pewność, że element został już zamontowany w DOM.
import React, { useRef, useEffect } from \"react\";
function FocusInput() {
// 1. Stwórz ref
const inputRef = useRef(null);
// Użyj useEffect, aby ustawić focus po zamontowaniu
useEffect(() => {
// 3. Dostęp do elementu DOM przez .current
if (inputRef.current) {
inputRef.current.focus();
console.log(\"Input DOM node:\", inputRef.current);
}
}, []); // Pusta tablica - tylko przy montowaniu
return (
<div>
<h2>Automatyczny Focus</h2>
{/* 2. Przypisz ref do atrybutu ref elementu */}
<input ref={inputRef} type=\"text\" placeholder=\"Mam focus!\" />
</div>
);
}
export default FocusInput;
2. Przechowywanie Dowolnej Mutowalnej Wartości (bez ponownego renderowania)
Czasami potrzebujemy przechowywać pewną wartość między renderowaniami, ale jej zmiana nie powinna wywoływać ponownego renderowania komponentu. Przykłady:
- Przechowywanie ID timera (
setTimeout
,setInterval
). - Przechowywanie poprzedniej wartości stanu lub propsów.
- Przechowywanie instancji obiektu, która ma być stała przez cały cykl życia komponentu.
import React, { useState, useEffect, useRef } from \"react\";
function Timer() {
const [seconds, setSeconds] = useState(0);
// Użyj useRef do przechowywania ID interwału
const intervalRef = useRef(null);
useEffect(() => {
// Rozpocznij interwał
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
console.log(\"Timer started, ID:\", intervalRef.current);
// Funkcja czyszcząca - użyj wartości z ref
return () => {
console.log(\"Clearing timer, ID:\", intervalRef.current);
clearInterval(intervalRef.current);
};
}, []); // Uruchom tylko raz
const handleStop = () => {
console.log(\"Stopping timer, ID:\", intervalRef.current);
clearInterval(intervalRef.current);
};
return (
<div>
<h2>Timer z useRef</h2>
<p>Sekundy: {seconds}</p>
<button onClick={handleStop}>Zatrzymaj Timer</button>
</div>
);
}
export default Timer;
W tym przykładzie, ID interwału jest przechowywane w intervalRef.current
. Zmiana tej wartości nie powoduje rerenderu, a jest ona dostępna w funkcji czyszczącej useEffect
oraz w handlerze handleStop
.
Przykład: Przechowywanie Poprzedniej Wartości Stanu
import React, { useState, useEffect, useRef } from \"react\";
function PreviousValue({ value }) {
const prevValueRef = useRef();
// Efekt uruchamiany PO renderowaniu
useEffect(() => {
// Zapisz aktualną wartość do ref, aby była dostępna jako \"poprzednia\" przy następnym renderowaniu
prevValueRef.current = value;
}); // Brak tablicy zależności - uruchom po każdym renderowaniu
const prevValue = prevValueRef.current;
return (
<p>
Aktualna wartość: {value}, Poprzednia wartość: {prevValue === undefined ? \"brak\" : prevValue}
</p>
);
}
// Użycie w App.jsx z licznikiem
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Zwiększ</button>
<PreviousValue value={count} />
</div>
);
}
`useRef` vs `useState`
| Cecha | `useRef` | `useState` | | :--------------------- | :---------------------------------------- | :--------------------------------------------- | | Cel główny | Dostęp do DOM, przechowywanie mutowalnych wartości | Zarządzanie stanem komponentu | | Powoduje rerender? | **Nie** | **Tak** (przy zmianie wartości) | | Wartość | Mutowalna (ref.current = ...
) | Niemutowalna (aktualizowana przez `setState`) |
| Dostęp do wartości | Synchroniczny (ref.current
) | Asynchroniczny (wartość dostępna w kolejnym renderze) |
| Użycie w renderowaniu | Odczyt wartości OK, ale zmiana nie wpłynie na UI | Odczyt wartości OK, zmiana wywoła aktualizację UI |
Przekazywanie Refów (Forwarding Refs)
Domyślnie nie można przekazać atrybutu ref
do komponentu funkcyjnego tak, jak zwykłego propsa. Jeśli chcesz, aby komponent nadrzędny mógł uzyskać dostęp do węzła DOM wewnątrz komponentu podrzędnego, musisz użyć React.forwardRef
.
import React, { useRef, forwardRef, useImperativeHandle } from \"react\";
// Komponent podrzędny opakowany w forwardRef
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// Opcjonalnie: Użyj useImperativeHandle, aby dostosować, co jest dostępne przez ref
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
// Można dodać inne metody
clear: () => {
inputRef.current.value = \"\";
}
}));
return <input ref={inputRef} {...props} />;
});
// Komponent nadrzędny
function ParentComponent() {
const fancyInputRef = useRef();
const handleFocus = () => {
// Wywołanie metody \"focus\" udostępnionej przez FancyInput
if (fancyInputRef.current) {
fancyInputRef.current.focus();
}
};
const handleClear = () => {
if (fancyInputRef.current) {
fancyInputRef.current.clear();
}
};
return (
<div>
<FancyInput ref={fancyInputRef} placeholder=\"Przekazany ref\" />
<button onClick={handleFocus}>Ustaw Focus na FancyInput</button>
<button onClick={handleClear}>Wyczyść FancyInput</button>
</div>
);
}
export default ParentComponent;
forwardRef
pozwala komponentowi "przekazać dalej" ref otrzymany od rodzica do elementu DOM wewnątrz siebie. useImperativeHandle
(używany wewnątrz komponentu z forwardRef
) pozwala dostosować, jakie metody lub wartości są eksponowane przez ref rodzicowi, zamiast udostępniać cały węzeł DOM.
Ćwiczenie praktyczne
Pokaż rozwiązanie
// src/VideoPlayer.jsx
import React, { useRef } from \"react\";
// Przykładowy link do wideo (może wymagać zastąpienia działającym linkiem)
const VIDEO_SRC = \"https://www.w3schools.com/html/mov_bbb.mp4\";
function VideoPlayer() {
const videoRef = useRef(null);
const handlePlay = () => {
if (videoRef.current) {
videoRef.current.play()
.catch(error => console.error(\"Błąd odtwarzania:\", error)); // Obsługa błędu, np. gdy użytkownik nie interagował jeszcze ze stroną
} else {
console.warn(\"Video ref not available\");
}
};
const handlePause = () => {
if (videoRef.current) {
videoRef.current.pause();
} else {
console.warn(\"Video ref not available\");
}
};
return (
<div>
<h2>Odtwarzacz Wideo</h2>
<video
ref={videoRef}
src={VIDEO_SRC}
width=\"400\"
controls // Pokaż domyślne kontrolki dla porównania
>
Twoja przeglądarka nie obsługuje tagu video.
</video>
<div style={{ marginTop: \"10px\" }}>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause} style={{ marginLeft: \"5px\" }}>Pause</button>
</div>
</div>
);
}
export default VideoPlayer;
// W App.jsx
import React from \"react\";
import VideoPlayer from \"./VideoPlayer\";
function App() {
return (
<div>
<VideoPlayer />
</div>
);
}
export default App;
Cel: Stworzyć komponent odtwarzacza wideo, który używa useRef
do uzyskania dostępu do elementu <video>
i dodaje przyciski "Play" i "Pause".
Kroki:
- Stwórz komponent funkcyjny
VideoPlayer
. - Użyj
useRef
do stworzenia referencji (np.videoRef
) dla elementu<video>
. - Wyrenderuj element
<video>
z atrybutemref={videoRef}
. Dodaj atrybutsrc
z linkiem do przykładowego pliku wideo (możesz użyć np. linku z sieci lub lokalnego pliku, jeśli masz skonfigurowany serwer). Dodaj atrybutcontrols
, aby zobaczyć domyślne kontrolki. - Dodaj dwa przyciski: "Play" i "Pause".
- Stwórz handlery
handlePlay
ihandlePause
dla tych przycisków. - W handlerach użyj
videoRef.current
, aby wywołać odpowiednie metody na elemencie wideo:videoRef.current.play()
ivideoRef.current.pause()
. Pamiętaj o sprawdzeniu, czyvideoRef.current
istnieje. - Użyj komponentu
VideoPlayer
wApp.jsx
.
Zadanie do samodzielnego wykonania
Stwórz komponent ScrollToTopButton
, który:
- Wyświetla przycisk "Do góry".
- Przycisk jest widoczny tylko wtedy, gdy użytkownik przewinął stronę w dół o określoną wartość (np. 200 pikseli). Użyj
useState
do przechowywania widoczności przycisku iuseEffect
do nasłuchiwania zdarzeniascroll
nawindow
. - Gdy użytkownik kliknie przycisk, strona powinna płynnie przewinąć się do góry (
window.scrollTo({ top: 0, behavior: \"smooth\" })
). - Użyj
useRef
do przechowywania ID timera, jeśli zdecydujesz się na debouncing/throttling dla handlera scrolla (opcjonalne, dla optymalizacji).
FAQ - Hook useRef
Czy mogę używać `useRef` do przechowywania stanu, który wpływa na renderowanie?
Nie powinno się. Jeśli zmiana wartości ma spowodować aktualizację UI, należy użyć `useState` lub `useReducer`. `useRef` jest przeznaczony do wartości, których zmiana *nie* powinna wywoływać ponownego renderowania.
Kiedy dokładnie `ref.current` jest ustawiany przy dostępie do DOM?
React ustawia wartość `ref.current` po zamontowaniu komponentu (po pierwszym renderowaniu) i zeruje ją tuż przed odmontowaniem. Dlatego bezpieczny dostęp do `ref.current` w celu interakcji z DOM zazwyczaj odbywa się wewnątrz `useEffect` lub handlerów zdarzeń.
Czy mogę przypisać ref do komponentu funkcyjnego?
Bezpośrednio nie. Jeśli spróbujesz zrobić `<MyFunctionComponent ref={myRef} />`, otrzymasz ostrzeżenie. Aby przekazać ref do komponentu funkcyjnego (np. w celu uzyskania dostępu do elementu DOM wewnątrz niego), komponent ten musi być opakowany w `React.forwardRef`.
Jaka jest różnica między `useRef` a tworzeniem obiektu `{ current: ... }` ręcznie?
`useRef` gwarantuje, że ten sam obiekt referencji jest zwracany przy każdym renderowaniu komponentu. Ręczne tworzenie obiektu `{ current: ... }` w ciele komponentu spowodowałoby tworzenie nowego obiektu przy każdym renderowaniu, tracąc trwałość wartości między renderowaniami.
Czy `useRef` jest tylko do DOM?
Nie, chociaż dostęp do DOM jest jego głównym zastosowaniem, `useRef` jest uniwersalnym narzędziem do przechowywania dowolnej mutowalnej wartości, która musi przetrwać między renderowaniami bez powodowania ich ponownego wywołania (np. ID timerów, poprzednie wartości stanu, instancje obiektów).
Czy zmiana `ref.current` w trakcie renderowania jest bezpieczna?
Technicznie możliwa, ale generalnie odradzana. Renderowanie powinno być czystym procesem. Modyfikowanie refów w trakcie renderowania może prowadzić do nieprzewidywalnych zachowań. Lepiej modyfikować refy wewnątrz `useEffect` lub handlerów zdarzeń.
Do czego służy `useImperativeHandle`?
`useImperativeHandle` jest używany w połączeniu z `forwardRef`. Pozwala komponentowi podrzędnemu dostosować, co dokładnie jest eksponowane rodzicowi przez ref. Zamiast udostępniać cały węzeł DOM, można udostępnić tylko wybrane metody (np. `focus`, `play`, `pause`), co jest lepsze dla enkapsulacji.