Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Referencje z useRef
W tej lekcji8
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.
Czym jest useRef?
Hook useRef jest jednym z wbudowanych hooków w React, który:
- Tworzy mutowalne pole referencji, które przetrwa przez cały cykl życia komponentu
- Nie powoduje ponownego renderowania komponentu przy zmianie jego wartości
- Może być używany do przechowywania odniesień do elementów DOM lub dowolnych innych wartości
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.
Główne przypadki użycia useRef
1. Dostęp do elementów DOM
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:
- Tworzymy referencję
thrusterControlRefza pomocąuseRef(null) - Przypisujemy tę referencję do elementu
inputza pomocą atrybuturef - Używamy referencji, aby wywołać metodę
focus()na elemencie DOM
2. Przechowywanie wartości mutable między renderowaniami
useRef może służyć do przechowywania wartości, które:
- Nie powinny powodować ponownego renderowania przy zmianie
- Muszą być stabilne przez cały cykl życia komponentu
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ż:
- Nie potrzebujemy ponownego renderowania przy zmianie ID interwału
- Musimy zachować ID między renderowaniami, aby móc zatrzymać interwał
- Wartość ta nie jest używana bezpośrednio w renderowanym JSX
3. Śledzenie poprzednich wartości
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.
4. Unikanie zbędnych efektów
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}useRef vs useState - kiedy używać?
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 |
Kiedy użyć useRef:
- Dostęp do elementów DOM
- Przechowywanie ID timerów i interwałów
- Przechowywanie poprzednich wartości
- Wartości, które zmieniają się często, ale nie wpływają na UI
- Przechowywanie instancji klas i obiektów, które nie powinny być tworzone przy każdym renderowaniu
Kiedy użyć useState:
- Dane, które wpływają na to, co widzi użytkownik
- Wartości, których zmiana powinna powodować aktualizację interfejsu
- Stan, który kontroluje zachowanie lub wygląd interfejsu użytkownika
Zaawansowane wzorce używania useRef
1. Callback Refs
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.
2. Łączenie useRef z forwardRef
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.
3. Przechowywanie wielu referencji
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.
Typowe pułapki przy używaniu useRef
1. Próba renderowania bezpośrednio z ref.current
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}2. Zapominanie o .current
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}3. Niewłaściwe użycie ref do sterowania renderowaniem
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}Najlepsze praktyki z useRef
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
nulldla 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ć
nullpodczas pierwszego renderowania.Używaj useCallback dla funkcji tworzących referencje - jeśli przekazujesz funkcję referencji, rozważ owinięcie jej w
useCallbackdla optymalizacji.Unikaj nadużywania refs - chociaż refs są przydatne, nadmierne poleganie na nich może prowadzić do trudnego w utrzymaniu kodu.
Przykład praktyczny: Zaawansowany odtwarzacz wideo kosmicznej misji
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:
- Dostęp do elementu DOM (
videoRef,progressBarRef) - Przechowywanie ID animacji (
animationFrameRef) - Flaga montowania (
isMountedRef) - Śledzenie wartości dla throttlingu (
lastUpdateTimeRef)
- Dostęp do elementu DOM (
Integracja referencji z efektami i callbackami
Bezpośrednia manipulacja elementem wideo przez jego API
Throttling aktualizacji stanu za pomocą referencji czasu
Podsumowanie
Hook useRef jest niezbędnym narzędziem w zestawie React, szczególnie przydatnym do:
- Przechowywania referencji do elementów DOM
- Utrzymywania wartości między renderowaniami bez wywoływania ponownego renderowania
- Śledzenia poprzednich wartości
- Przechowywania ID timerów, interwałów i requestAnimationFrame
- Przechowywania flag i wartości, które nie wpływają bezpośrednio na interfejs
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ą.
Kod do tej lekcji: App.jsx
1import React, { useState, useRef, useEffect } from 'react';
2
3// useRef do przechowywania referencji do elementów DOM
4function DOMReferenceDemo() {
5 const inputRef = useRef(null);
6 const canvasRef = useRef(null);
7
8 const focusInput = () => {
9 inputRef.current?.focus();
10 };
11
12 const drawOnCanvas = () => {
13 const canvas = canvasRef.current;
14 if (!canvas) return;
15
16 const ctx = canvas.getContext('2d');
17 ctx.clearRect(0, 0, canvas.width, canvas.height);
18
19 // Rysowanie gwiazd
20 for (let i = 0; i < 50; i++) {
21 const x = Math.random() * canvas.width;
22 const y = Math.random() * canvas.height;
23 const radius = Math.random() * 2;
24
25 ctx.beginPath();
26 ctx.arc(x, y, radius, 0, Math.PI * 2);
27 ctx.fillStyle = `rgba(255, 255, 255, ${Math.random()})`;
28 ctx.fill();
29 }
30
31 // Rysowanie planety
32 ctx.beginPath();
33 ctx.arc(canvas.width / 2, canvas.height / 2, 30, 0, Math.PI * 2);
34 ctx.fillStyle = '#61dafb';
35 ctx.fill();
36 };
37
38 return (
39 <div className="demo-card">
40 <h2>Referencje DOM</h2>
41
42 <div className="input-section">
43 <input ref={inputRef} type="text" placeholder="Kliknij przycisk poniżej" />
44 <button onClick={focusInput}>Fokus na input</button>
45 </div>
46
47 <div className="canvas-section">
48 <canvas ref={canvasRef} width={280} height={150} />
49 <button onClick={drawOnCanvas}>Rysuj kosmos</button>
50 </div>
51 </div>
52 );
53}
54
55// useRef do przechowywania wartości między renderami
56function PreviousValueDemo() {
57 const [count, setCount] = useState(0);
58 const prevCountRef = useRef(0);
59
60 useEffect(() => {
61 prevCountRef.current = count;
62 });
63
64 return (
65 <div className="demo-card">
66 <h2>Poprzednia wartość</h2>
67 <div className="values">
68 <div className="value-box">
69 <span className="label">Aktualna:</span>
70 <span className="number current">{count}</span>
71 </div>
72 <div className="value-box">
73 <span className="label">Poprzednia:</span>
74 <span className="number previous">{prevCountRef.current}</span>
75 </div>
76 </div>
77 <button onClick={() => setCount(c => c + 1)}>Zwiększ</button>
78 </div>
79 );
80}
81
82// useRef do mierzenia elementów
83function MeasurementDemo() {
84 const boxRef = useRef(null);
85 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
86 const [boxSize, setBoxSize] = useState(100);
87
88 const measureBox = () => {
89 if (boxRef.current) {
90 const rect = boxRef.current.getBoundingClientRect();
91 setDimensions({ width: rect.width, height: rect.height });
92 }
93 };
94
95 return (
96 <div className="demo-card">
97 <h2>Pomiary elementów</h2>
98 <div
99 ref={boxRef}
100 className="measure-box"
101 style={{ width: boxSize, height: boxSize }}
102 >
103 {boxSize}x{boxSize}
104 </div>
105 <div className="measurement-info">
106 Zmierzono: {dimensions.width.toFixed(0)} x {dimensions.height.toFixed(0)}
107 </div>
108 <div className="controls">
109 <button onClick={() => setBoxSize(s => Math.max(50, s - 20))}></button>
110 <button onClick={measureBox}>Zmierz</button>
111 <button onClick={() => setBoxSize(s => Math.min(200, s + 20))}></button>
112 </div>
113 </div>
114 );
115}
116
117// useRef do przechowywania timeoutów/intervalów
118function TimerRefDemo() {
119 const [time, setTime] = useState(0);
120 const [isRunning, setIsRunning] = useState(false);
121 const intervalRef = useRef(null);
122
123 const start = () => {
124 if (intervalRef.current) return;
125
126 setIsRunning(true);
127 intervalRef.current = setInterval(() => {
128 setTime(t => t + 1);
129 }, 100);
130 };
131
132 const stop = () => {
133 if (intervalRef.current) {
134 clearInterval(intervalRef.current);
135 intervalRef.current = null;
136 }
137 setIsRunning(false);
138 };
139
140 const reset = () => {
141 stop();
142 setTime(0);
143 };
144
145 // Cleanup przy odmontowaniu
146 useEffect(() => {
147 return () => {
148 if (intervalRef.current) {
149 clearInterval(intervalRef.current);
150 }
151 };
152 }, []);
153
154 const formatTime = (t) => {
155 const seconds = Math.floor(t / 10);
156 const decimals = t % 10;
157 return `${seconds}.${decimals}s`;
158 };
159
160 return (
161 <div className="demo-card">
162 <h2>⏱Timer z useRef</h2>
163 <div className="timer-display">{formatTime(time)}</div>
164 <div className="controls">
165 <button onClick={start} disabled={isRunning}>▶Start</button>
166 <button onClick={stop} disabled={!isRunning}>⏸Pauza</button>
167 <button onClick={reset}>Reset</button>
168 </div>
169 <p className="hint">Interval ID przechowywany w useRef</p>
170 </div>
171 );
172}
173
174function App() {
175 return (
176 <div className="app">
177 <h1>useRef Hook</h1>
178 <p className="subtitle">Referencje i wartości poza cyklem renderowania</p>
179 <div className="demos">
180 <DOMReferenceDemo />
181 <PreviousValueDemo />
182 <MeasurementDemo />
183 <TimerRefDemo />
184 </div>
185 </div>
186 );
187}
188
189export default App;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zawiera tablica zależności (dependency array) w useEffect?
2. Które z poniższych jest przykładem efektu ubocznego (side effect) w React?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż elementy deklaracji useRef od lewej do prawej:
- Układanie w pionie
Uporządkuj kroki prawidłowego użycia async/await wewnątrz useEffect:
- Klikanie w kolejności
Uporządkuj async operacje w useEffect:
- Edytor kodu
Śledzenie pozycji geolokalizacji