Kurs JavaScript i React · Moduł 5: useEffect i własne hooki

Referencje z useRef

12 min czytania
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:

  1. Tworzy mutowalne pole referencji, które przetrwa przez cały cykl życia komponentu
  2. Nie powoduje ponownego renderowania komponentu przy zmianie jego wartości
  3. 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:

  1. Tworzymy referencję thrusterControlRef za pomocą useRef(null)
  2. Przypisujemy tę referencję do elementu input za pomocą atrybutu ref
  3. 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:

AspektuseRefuseState
Powoduje ponowne renderowanieNieTak
Zachowuje wartość między renderowaniamiTakTak
Dostęp do wartościPrzez właściwość .currentBezpośrednio przez odczytanie zmiennej stanu
Aktualizacja wartościBezpośrednia mutacja ref.current = newValuePrzez funkcję aktualizującą setState(newValue)
Kiedy użyć?Dla wartości, które nie wpływają na wyjście renderowaniaDla 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

  1. 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.

  2. Zachowaj ostrożność przy bezpośredniej manipulacji DOM - manipulacja DOM przez referencje powinna być ograniczona i dobrze przemyślana.

  3. Inicjalizuj refs z odpowiednią wartością początkową - na przykład null dla referencji DOM lub początkowa wartość dla liczników.

  4. Sprawdzaj czy ref.current istnieje przed użyciem - szczególnie w przypadku referencji DOM, które mogą być null podczas pierwszego renderowania.

  5. Używaj useCallback dla funkcji tworzących referencje - jeśli przekazujesz funkcję referencji, rozważ owinięcie jej w useCallback dla optymalizacji.

  6. 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:

  1. 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)
  2. Integracja referencji z efektami i callbackami

  3. Bezpośrednia manipulacja elementem wideo przez jego API

  4. 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. 1. Co zawiera tablica zależności (dependency array) w useEffect?

  2. 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

Przydatne artykuły