Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Referencje z useRef

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:

| 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

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

Ir a CodeWorlds