W kosmicznej podróży przez React, czasami potrzebujemy utrzymywać pewne dane lub odniesienia do elementów DOM, które przetrwają między renderowaniami, ale nie powodują ponownego renderowania komponentu. Hook
useRef jest jak magazyn na statku kosmicznym - pozwala przechowywać ważne rzeczy, które nie zmieniają się przy każdym manewrze statku.Hook
useRef jest jednym z wbudowanych hooków w React, który:Podstawowa składnia:
1const refContainer = useRef(initialValue);Zwrócony obiekt
refContainer ma właściwość .current, która jest inicjalizowana wartością początkową (initialValue). Wartość ta może być zmieniana bez wywoływania ponownego renderowania.Jednym z najpopularniejszych zastosowań
useRef jest uzyskanie bezpośredniego dostępu do węzłów DOM:1function SpaceshipControls() {
2 // Utworzenie referencji
3 const thrusterControlRef = useRef(null);
4
5 // Funkcja używająca referencji do elementu DOM
6 const focusThrusterControl = () => {
7 // Dostęp do elementu DOM przez właściwość .current
8 thrusterControlRef.current.focus();
9 };
10
11 return (
12 <div className="control-panel">
13 <h2>Panel sterowania napędem</h2>
14
15 {/* Przypisanie referencji do elementu input */}
16 <input
17 ref={thrusterControlRef}
18 type="range"
19 min="0"
20 max="100"
21 className="thruster-slider"
22 />
23
24 <button onClick={focusThrusterControl}>
25 Aktywuj kontrolę ciągu
26 </button>
27 </div>
28 );
29}W tym przykładzie:
thrusterControlRef za pomocą useRef(null)input za pomocą atrybutu reffocus() na elemencie DOMuseRef może służyć do przechowywania wartości, które:1function MissionTimer() {
2 const [seconds, setSeconds] = useState(0);
3 const timerIdRef = useRef(null);
4
5 // Funkcja rozpoczynająca timer
6 const startTimer = () => {
7 if (timerIdRef.current !== null) return; // Unikaj duplikacji timerów
8
9 timerIdRef.current = setInterval(() => {
10 setSeconds(s => s + 1);
11 }, 1000);
12 };
13
14 // Funkcja zatrzymująca timer
15 const stopTimer = () => {
16 clearInterval(timerIdRef.current);
17 timerIdRef.current = null;
18 };
19
20 // Czyszczenie interwału przy odmontowaniu
21 useEffect(() => {
22 return () => {
23 if (timerIdRef.current !== null) {
24 clearInterval(timerIdRef.current);
25 }
26 };
27 }, []);
28
29 return (
30 <div className="mission-timer">
31 <p>Czas misji: {seconds} sekund</p>
32 <button onClick={startTimer}>Start</button>
33 <button onClick={stopTimer}>Stop</button>
34 </div>
35 );
36}W tym przykładzie używamy
useRef do przechowywania ID interwału. Jest to idealne zastosowanie, ponieważ:Jednym z ciekawych zastosowań
useRef jest śledzenie poprzednich wartości props lub stanu:1function usePrevious(value) {
2 const ref = useRef();
3
4 // Aktualizacja ref.current po renderowaniu
5 useEffect(() => {
6 ref.current = value;
7 }, [value]);
8
9 // Zwraca poprzednią wartość (undefined podczas pierwszego renderowania)
10 return ref.current;
11}
12
13function AsteroidTracker({ asteroidId }) {
14 const [asteroidData, setAsteroidData] = useState(null);
15 // Śledzenie poprzedniego ID
16 const previousAsteroidId = usePrevious(asteroidId);
17
18 // Efekt, który loguje zmianę obserwowanego asteroidu
19 useEffect(() => {
20 if (previousAsteroidId !== asteroidId) {
21 console.log(`Zmiana śledzenia z asteroidy ${previousAsteroidId} na ${asteroidId}`);
22 // Tutaj moglibyśmy pobierać nowe dane o asteroidzie
23 }
24 }, [asteroidId, previousAsteroidId]);
25
26 // Renderowanie...
27}Hook
usePrevious używa useRef do przechowywania poprzedniej wartości. Aktualizacja referencji odbywa się w useEffect, który uruchamia się po renderowaniu, więc wartość zapisana w referencji jest zawsze o jeden cykl renderowania "opóźniona" względem bieżącej wartości.useRef może pomóc w optymalizacji, pozwalając uniknąć zbędnych efektów:1function TelemetryMonitor({ shipId }) {
2 const [data, setData] = useState(null);
3 const [isConnected, setIsConnected] = useState(false);
4
5 // Przechowywanie ID, żeby uniknąć zbędnych zmian połączenia
6 const lastShipIdRef = useRef(shipId);
7
8 useEffect(() => {
9 // Sprawdzenie, czy ID statku rzeczywiście się zmieniło
10 if (lastShipIdRef.current === shipId && isConnected) {
11 console.log('Statek nie zmienił się, pomijam ponowne łączenie');
12 return;
13 }
14
15 // Aktualizacja ref z nowym ID
16 lastShipIdRef.current = shipId;
17
18 // Nawiązanie połączenia...
19 console.log(`Łączenie z telemetrią statku ${shipId}...`);
20
21 const connection = telemetryService.connect(shipId);
22 setIsConnected(true);
23
24 connection.onData(newData => {
25 setData(newData);
26 });
27
28 return () => {
29 console.log(`Rozłączanie telemetrii statku ${shipId}...`);
30 connection.disconnect();
31 setIsConnected(false);
32 };
33 }, [shipId]);
34
35 // Renderowanie...
36}Różnica między
useRef a useState jest kluczowa do zrozumienia:| Aspekt | useRef | useState | |--------|--------|----------| | Powoduje ponowne renderowanie | Nie | Tak | | Zachowuje wartość między renderowaniami | Tak | Tak | | Dostęp do wartości | Przez właściwość
.current | Bezpośrednio przez odczytanie zmiennej stanu |
| Aktualizacja wartości | Bezpośrednia mutacja ref.current = newValue | Przez funkcję aktualizującą setState(newValue) |
| Kiedy użyć? | Dla wartości, które nie wpływają na wyjście renderowania | Dla wartości, które powinny powodować ponowne renderowanie |Czasami potrzebujemy większej kontroli nad tym, kiedy ref jest przypisywany. W takich przypadkach możemy użyć callback ref:
1function SpectrumAnalyzer() {
2 const [frequency, setFrequency] = useState(440);
3 let canvasRef = useRef(null);
4
5 // Funkcja callback dla ref, wywoływana gdy ref jest przypisany lub odłączony
6 const setCanvasRef = useCallback(element => {
7 // element będzie null podczas odmontowywania
8 if (element) {
9 canvasRef.current = element;
10 // Możemy teraz wykonać inicjalizację po przypisaniu referencji
11 initializeCanvas(element, frequency);
12 } else {
13 // Czyszczenie zasobów przy odmontowaniu
14 if (canvasRef.current) {
15 cleanupCanvas(canvasRef.current);
16 }
17 canvasRef.current = null;
18 }
19 }, [frequency]); // Zależność od frequency
20
21 // Funkcja inicjalizująca canvas
22 const initializeCanvas = (canvas, freq) => {
23 console.log(`Inicjalizacja analizatora dla częstotliwości ${freq} Hz`);
24 const ctx = canvas.getContext('2d');
25 // Rysowanie...
26 };
27
28 return (
29 <div>
30 <canvas
31 ref={setCanvasRef}
32 width="600"
33 height="300"
34 />
35 <input
36 type="range"
37 min="20"
38 max="20000"
39 value={frequency}
40 onChange={e => setFrequency(Number(e.target.value))}
41 />
42 <div>Częstotliwość: {frequency} Hz</div>
43 </div>
44 );
45}Callback ref daje nam większą kontrolę, ponieważ jest wywoływany zarówno przy przypisywaniu, jak i przy usuwaniu referencji.
Czasami chcemy przekazać referencję do komponentu potomnego. Możemy to zrobić za pomocą
forwardRef:1// Komponent potomny
2const NavigationControl = forwardRef((props, ref) => {
3 return (
4 <div className="nav-control">
5 <label>{props.label}</label>
6 <input
7 ref={ref}
8 type="text"
9 className="nav-input"
10 value={props.value}
11 onChange={props.onChange}
12 />
13 </div>
14 );
15});
16
17// Komponent rodzic
18function NavigationPanel() {
19 const destinationInputRef = useRef(null);
20 const [destination, setDestination] = useState('');
21
22 const focusDestination = () => {
23 // Możemy uzyskać dostęp do wewnętrznego inputa dzięki forwardRef
24 destinationInputRef.current.focus();
25 };
26
27 return (
28 <div className="nav-panel">
29 <h2>Panel nawigacyjny</h2>
30
31 <NavigationControl
32 ref={destinationInputRef}
33 label="Cel podróży:"
34 value={destination}
35 onChange={e => setDestination(e.target.value)}
36 />
37
38 <button onClick={focusDestination}>
39 Ustaw cel
40 </button>
41 </div>
42 );
43}forwardRef pozwala komponentowi otrzymać referencję od rodzica i przekazać ją do określonego elementu wewnątrz komponentu.Czasami potrzebujemy śledzić wiele elementów, na przykład listę elementów:
1function StarMap({ stars }) {
2 // Przechowywanie mapy referencji do gwiazd
3 const starRefs = useRef({});
4
5 // Funkcja do przewinięcia do określonej gwiazdy
6 const scrollToStar = (starId) => {
7 if (starRefs.current[starId]) {
8 starRefs.current[starId].scrollIntoView({
9 behavior: 'smooth',
10 block: 'center'
11 });
12 }
13 };
14
15 return (
16 <div className="star-map-container">
17 <div className="star-list">
18 {stars.map(star => (
19 <div
20 key={star.id}
21 // Zapisujemy referencję do elementu w naszej mapie
22 ref={el => starRefs.current[star.id] = el}
23 className="star-item"
24 >
25 <h3>{star.name}</h3>
26 <p>Typ: {star.type}</p>
27 <p>Odległość: {star.distance} lat świetlnych</p>
28 </div>
29 ))}
30 </div>
31
32 <div className="star-navigator">
33 <h3>Szybka nawigacja</h3>
34 {stars.map(star => (
35 <button
36 key={star.id}
37 onClick={() => scrollToStar(star.id)}
38 >
39 {star.name}
40 </button>
41 ))}
42 </div>
43 </div>
44 );
45}W tym przykładzie przechowujemy mapę referencji, gdzie kluczem jest
id gwiazdy, a wartością - referencja do odpowiedniego elementu DOM.Ponieważ zmiana
ref.current nie powoduje ponownego renderowania, próba wykorzystania tej wartości bezpośrednio w JSX może prowadzić do niespójności:1// ŹLE: Zmiana ref.current nie spowoduje aktualizacji interfejsu
2function BadCounter() {
3 const countRef = useRef(0);
4
5 const increment = () => {
6 // Ta zmiana nie spowoduje ponownego renderowania
7 countRef.current += 1;
8 console.log(`Licznik: ${countRef.current}`); // Zostanie zalogowane prawidłowo
9 };
10
11 return (
12 <div>
13 <p>Licznik: {countRef.current}</p> {/* Nie zaktualizuje się po kliknięciu */}
14 <button onClick={increment}>Zwiększ</button>
15 </div>
16 );
17}
18
19// DOBRZE: Użyj useState dla wartości, które wpływają na renderowanie
20function GoodCounter() {
21 const [count, setCount] = useState(0);
22
23 const increment = () => {
24 setCount(c => c + 1);
25 };
26
27 return (
28 <div>
29 <p>Licznik: {count}</p> {/* Zaktualizuje się po kliknięciu */}
30 <button onClick={increment}>Zwiększ</button>
31 </div>
32 );
33}Częstym błędem jest zapominanie o dostępie do wartości poprzez
.current:1// ŹLE: Brak .current
2function MissionLog({ onAddLog }) {
3 const logInputRef = useRef(null);
4
5 const handleAddLog = () => {
6 // Błąd: Zapomnieliśmy o .current
7 const logText = logInputRef.value;
8 onAddLog(logText);
9 };
10
11 return (
12 <div>
13 <input ref={logInputRef} type="text" />
14 <button onClick={handleAddLog}>Dodaj wpis</button>
15 </div>
16 );
17}
18
19// DOBRZE: Używamy .current
20function MissionLog({ onAddLog }) {
21 const logInputRef = useRef(null);
22
23 const handleAddLog = () => {
24 // Poprawne użycie .current
25 const logText = logInputRef.current.value;
26 onAddLog(logText);
27 logInputRef.current.value = '';
28 };
29
30 return (
31 <div>
32 <input ref={logInputRef} type="text" />
33 <button onClick={handleAddLog}>Dodaj wpis</button>
34 </div>
35 );
36}useRef nie jest odpowiednim narzędziem do kontrolowania, kiedy komponent powinien się renderować:1// ŹLE: Próba użycia ref do sterowania renderowaniem
2function BadRenderControl() {
3 const shouldUpdateRef = useRef(false);
4
5 const forceUpdate = () => {
6 shouldUpdateRef.current = true;
7 // Brakuje mechanizmu wymuszającego ponowne renderowanie
8 };
9
10 console.log('Renderowanie', shouldUpdateRef.current);
11
12 // To nie zadziała jak oczekiwano
13 if (shouldUpdateRef.current) {
14 shouldUpdateRef.current = false;
15 // Jakieś operacje...
16 }
17
18 return <button onClick={forceUpdate}>Odśwież</button>;
19}
20
21// DOBRZE: Użycie useState do sterowania renderowaniem
22function GoodRenderControl() {
23 const [update, setUpdate] = useState(0);
24 const didUpdateRef = useRef(false);
25
26 const forceUpdate = () => {
27 setUpdate(u => u + 1); // Wywołuje ponowne renderowanie
28 };
29
30 console.log('Renderowanie', update);
31
32 useEffect(() => {
33 if (update > 0) {
34 didUpdateRef.current = true;
35 // Operacje po aktualizacji...
36 }
37 }, [update]);
38
39 return <button onClick={forceUpdate}>Odśwież</button>;
40}Używaj useRef do wartości, które nie wpływają na render - jeśli zmiana wartości powinna powodować aktualizację interfejsu, użyj
useState.Zachowaj ostrożność przy bezpośredniej manipulacji DOM - manipulacja DOM przez referencje powinna być ograniczona i dobrze przemyślana.
Inicjalizuj refs z odpowiednią wartością początkową - na przykład
null dla referencji DOM lub początkowa wartość dla liczników.Sprawdzaj czy ref.current istnieje przed użyciem - szczególnie w przypadku referencji DOM, które mogą być
null podczas pierwszego renderowania.Używaj useCallback dla funkcji tworzących referencje - jeśli przekazujesz funkcję referencji, rozważ owinięcie jej w
useCallback dla optymalizacji.Unikaj nadużywania refs - chociaż refs są przydatne, nadmierne poleganie na nich może prowadzić do trudnego w utrzymaniu kodu.
Poniżej znajduje się bardziej złożony przykład, który pokazuje różne aspekty korzystania z
useRef:1import React, { useState, useRef, useEffect, useCallback } from 'react';
2
3function MissionVideoPlayer({ videoUrl, onProgress }) {
4 const [isPlaying, setIsPlaying] = useState(false);
5 const [duration, setDuration] = useState(0);
6 const [currentTime, setCurrentTime] = useState(0);
7 const [volume, setVolume] = useState(0.7);
8
9 // Referencje
10 const videoRef = useRef(null); // Referencja do elementu wideo
11 const progressBarRef = useRef(null); // Referencja do paska postępu
12 const animationFrameRef = useRef(null); // ID requestAnimationFrame do aktualizacji czasu
13 const isMountedRef = useRef(true); // Flaga, czy komponent jest zamontowany
14 const lastUpdateTimeRef = useRef(0); // Czas ostatniej aktualizacji dla throttlingu
15
16 // Funkcja inicjalizująca metadane wideo
17 const handleLoadedMetadata = () => {
18 if (videoRef.current) {
19 setDuration(videoRef.current.duration);
20 videoRef.current.volume = volume;
21 }
22 };
23
24 // Funkcja aktualizująca pasek postępu
25 const updateProgressBar = useCallback(() => {
26 if (!videoRef.current || !isMountedRef.current) return;
27
28 const now = Date.now();
29 if (now - lastUpdateTimeRef.current > 200) { // Throttling do 5 aktualizacji na sekundę
30 setCurrentTime(videoRef.current.currentTime);
31 onProgress(videoRef.current.currentTime);
32 lastUpdateTimeRef.current = now;
33 }
34
35 // Kontynuuj aktualizację jeśli wideo jest odtwarzane
36 if (videoRef.current.paused) return;
37 animationFrameRef.current = requestAnimationFrame(updateProgressBar);
38 }, [onProgress]);
39
40 // Start lub pauza odtwarzania
41 const togglePlayPause = () => {
42 if (!videoRef.current) return;
43
44 if (videoRef.current.paused) {
45 videoRef.current.play();
46 setIsPlaying(true);
47 animationFrameRef.current = requestAnimationFrame(updateProgressBar);
48 } else {
49 videoRef.current.pause();
50 setIsPlaying(false);
51 if (animationFrameRef.current) {
52 cancelAnimationFrame(animationFrameRef.current);
53 }
54 }
55 };
56
57 // Zmiana pozycji odtwarzania po kliknięciu paska postępu
58 const handleProgressBarClick = (e) => {
59 if (!progressBarRef.current || !videoRef.current) return;
60
61 const rect = progressBarRef.current.getBoundingClientRect();
62 const position = (e.clientX - rect.left) / rect.width;
63 const newTime = duration * Math.max(0, Math.min(1, position));
64
65 videoRef.current.currentTime = newTime;
66 setCurrentTime(newTime);
67 };
68
69 // Zmiana głośności
70 const handleVolumeChange = (e) => {
71 const newVolume = parseFloat(e.target.value);
72 setVolume(newVolume);
73
74 if (videoRef.current) {
75 videoRef.current.volume = newVolume;
76 }
77 };
78
79 // Efekt czyszczący animationFrame i ustawiający flagę montowania
80 useEffect(() => {
81 return () => {
82 isMountedRef.current = false;
83 if (animationFrameRef.current) {
84 cancelAnimationFrame(animationFrameRef.current);
85 }
86 };
87 }, []);
88
89 // Formatowanie czasu (mm:ss)
90 const formatTime = (timeInSeconds) => {
91 const minutes = Math.floor(timeInSeconds / 60);
92 const seconds = Math.floor(timeInSeconds % 60);
93 return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
94 };
95
96 return (
97 <div className="mission-video-player">
98 <video
99 ref={videoRef}
100 src={videoUrl}
101 onLoadedMetadata={handleLoadedMetadata}
102 onEnded={() => setIsPlaying(false)}
103 />
104
105 <div className="controls">
106 <button onClick={togglePlayPause}>
107 {isPlaying ? 'Pauza' : 'Odtwórz'}
108 </button>
109
110 <div className="time-display">
111 {formatTime(currentTime)} / {formatTime(duration)}
112 </div>
113
114 <div
115 ref={progressBarRef}
116 className="progress-bar"
117 onClick={handleProgressBarClick}
118 >
119 <div
120 className="progress-fill"
121 style={{ width: `${(currentTime / duration) * 100}%` }}
122 />
123 </div>
124
125 <div className="volume-control">
126 <label>Głośność:</label>
127 <input
128 type="range"
129 min="0"
130 max="1"
131 step="0.05"
132 value={volume}
133 onChange={handleVolumeChange}
134 />
135 </div>
136 </div>
137 </div>
138 );
139}Ten przykład pokazuje kilka zaawansowanych aspektów pracy z
useRef:Wiele różnych zastosowań referencji:
videoRef, progressBarRef)animationFrameRef)isMountedRef)lastUpdateTimeRef)Integracja referencji z efektami i callbackami
Bezpośrednia manipulacja elementem wideo przez jego API
Throttling aktualizacji stanu za pomocą referencji czasu
Hook
useRef jest niezbędnym narzędziem w zestawie React, szczególnie przydatnym do:Kluczem do efektywnego wykorzystania
useRef jest zrozumienie, kiedy używać go zamiast useState. Zasadnicza różnica polega na tym, że useRef nie powoduje ponownego renderowania przy zmianie wartości, co czyni go idealnym wyborem dla operacji, które nie powinny wpływać na cykl renderowania.W naszej kosmicznej podróży przez React,
useRef jest jak magazyn lub schowek na statku - miejsce, gdzie możesz przechowywać rzeczy, które potrzebujesz mieć pod ręką, ale które nie wymagają uwagi całej załogi za każdym razem, gdy się zmieniają.