Lekcja 18: Hook useRef

Hook useRef jest kolejnym podstawowym Hookiem w React, ale jego zastosowanie różni się od useState, useEffect czy useContext. Głównym celem useRef jest umożliwienie dostępu do węzłów DOM lub przechowywanie dowolnej mutowalnej wartości, która nie powoduje ponownego renderowania komponentu przy jej zmianie.

Czym jest `useRef`?

useRef zwraca mutowalny obiekt referencji, którego właściwość .current jest inicjalizowana przekazanym argumentem (initialValue). Zwrócony obiekt będzie istniał przez cały cykl życia komponentu.

Składnia:**

import React, { useRef } from \"react\";

function MyComponent() {
  const myRef = useRef(initialValue);

  // Dostęp do wartości: myRef.current
  // Modyfikacja wartości: myRef.current = newValue;

  // ... reszta komponentu
}

Kluczowe cechy `useRef`:

Główne Zastosowania `useRef`

1. Dostęp do Węzłów DOM

Jest to najczęstsze zastosowanie useRef. Pozwala uzyskać bezpośredni dostęp do elementu DOM renderowanego przez React, aby móc wywoływać na nim metody imperatywne (np. focus(), blur(), pomiar rozmiaru, integracja z bibliotekami non-React).

Jak to działa:

  1. Stwórz referencję za pomocą useRef(null).
  2. Przekaż tę referencję do atrybutu ref elementu JSX, do którego chcesz uzyskać dostęp: <input ref={myInputRef} />.
  3. Gdy React renderuje element DOM, przypisze referencję do tego węzła DOM do właściwości .current obiektu ref (myInputRef.current będzie teraz wskazywać na element <input> w DOM).
  4. Możesz teraz uzyskać dostęp do węzła DOM przez myInputRef.current, ale zazwyczaj robisz to wewnątrz useEffect lub handlerów zdarzeń, aby mieć pewność, że element został już zamontowany w DOM.
import React, { useRef, useEffect } from \"react\";

function FocusInput() {
  // 1. Stwórz ref
  const inputRef = useRef(null);

  // Użyj useEffect, aby ustawić focus po zamontowaniu
  useEffect(() => {
    // 3. Dostęp do elementu DOM przez .current
    if (inputRef.current) {
      inputRef.current.focus();
      console.log(\"Input DOM node:\", inputRef.current);
    }
  }, []); // Pusta tablica - tylko przy montowaniu

  return (
    <div>
      <h2>Automatyczny Focus</h2>
      {/* 2. Przypisz ref do atrybutu ref elementu */}
      <input ref={inputRef} type=\"text\" placeholder=\"Mam focus!\" />
    </div>
  );
}

export default FocusInput;

2. Przechowywanie Dowolnej Mutowalnej Wartości (bez ponownego renderowania)

Czasami potrzebujemy przechowywać pewną wartość między renderowaniami, ale jej zmiana nie powinna wywoływać ponownego renderowania komponentu. Przykłady:

import React, { useState, useEffect, useRef } from \"react\";

function Timer() {
  const [seconds, setSeconds] = useState(0);
  // Użyj useRef do przechowywania ID interwału
  const intervalRef = useRef(null);

  useEffect(() => {
    // Rozpocznij interwał
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    console.log(\"Timer started, ID:\", intervalRef.current);

    // Funkcja czyszcząca - użyj wartości z ref
    return () => {
      console.log(\"Clearing timer, ID:\", intervalRef.current);
      clearInterval(intervalRef.current);
    };
  }, []); // Uruchom tylko raz

  const handleStop = () => {
    console.log(\"Stopping timer, ID:\", intervalRef.current);
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <h2>Timer z useRef</h2>
      <p>Sekundy: {seconds}</p>
      <button onClick={handleStop}>Zatrzymaj Timer</button>
    </div>
  );
}

export default Timer;

W tym przykładzie, ID interwału jest przechowywane w intervalRef.current. Zmiana tej wartości nie powoduje rerenderu, a jest ona dostępna w funkcji czyszczącej useEffect oraz w handlerze handleStop.

Przykład: Przechowywanie Poprzedniej Wartości Stanu

import React, { useState, useEffect, useRef } from \"react\";

function PreviousValue({ value }) {
  const prevValueRef = useRef();

  // Efekt uruchamiany PO renderowaniu
  useEffect(() => {
    // Zapisz aktualną wartość do ref, aby była dostępna jako \"poprzednia\" przy następnym renderowaniu
    prevValueRef.current = value;
  }); // Brak tablicy zależności - uruchom po każdym renderowaniu

  const prevValue = prevValueRef.current;

  return (
    <p>
      Aktualna wartość: {value}, Poprzednia wartość: {prevValue === undefined ? \"brak\" : prevValue}
    </p>
  );
}

// Użycie w App.jsx z licznikiem
function App() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>Zwiększ</button>
            <PreviousValue value={count} />
        </div>
    );
}

`useRef` vs `useState`

| Cecha | `useRef` | `useState` | | :--------------------- | :---------------------------------------- | :--------------------------------------------- | | Cel główny | Dostęp do DOM, przechowywanie mutowalnych wartości | Zarządzanie stanem komponentu | | Powoduje rerender? | **Nie** | **Tak** (przy zmianie wartości) | | Wartość | Mutowalna (ref.current = ...) | Niemutowalna (aktualizowana przez `setState`) | | Dostęp do wartości | Synchroniczny (ref.current) | Asynchroniczny (wartość dostępna w kolejnym renderze) | | Użycie w renderowaniu | Odczyt wartości OK, ale zmiana nie wpłynie na UI | Odczyt wartości OK, zmiana wywoła aktualizację UI |

Przekazywanie Refów (Forwarding Refs)

Domyślnie nie można przekazać atrybutu ref do komponentu funkcyjnego tak, jak zwykłego propsa. Jeśli chcesz, aby komponent nadrzędny mógł uzyskać dostęp do węzła DOM wewnątrz komponentu podrzędnego, musisz użyć React.forwardRef.

import React, { useRef, forwardRef, useImperativeHandle } from \"react\";

// Komponent podrzędny opakowany w forwardRef
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // Opcjonalnie: Użyj useImperativeHandle, aby dostosować, co jest dostępne przez ref
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    // Można dodać inne metody
    clear: () => {
        inputRef.current.value = \"\";
    }
  }));

  return <input ref={inputRef} {...props} />;
});

// Komponent nadrzędny
function ParentComponent() {
  const fancyInputRef = useRef();

  const handleFocus = () => {
    // Wywołanie metody \"focus\" udostępnionej przez FancyInput
    if (fancyInputRef.current) {
      fancyInputRef.current.focus();
    }
  };

   const handleClear = () => {
    if (fancyInputRef.current) {
      fancyInputRef.current.clear();
    }
  };

  return (
    <div>
      <FancyInput ref={fancyInputRef} placeholder=\"Przekazany ref\" />
      <button onClick={handleFocus}>Ustaw Focus na FancyInput</button>
      <button onClick={handleClear}>Wyczyść FancyInput</button>
    </div>
  );
}

export default ParentComponent;

forwardRef pozwala komponentowi "przekazać dalej" ref otrzymany od rodzica do elementu DOM wewnątrz siebie. useImperativeHandle (używany wewnątrz komponentu z forwardRef) pozwala dostosować, jakie metody lub wartości są eksponowane przez ref rodzicowi, zamiast udostępniać cały węzeł DOM.


Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/VideoPlayer.jsx
import React, { useRef } from \"react\";

// Przykładowy link do wideo (może wymagać zastąpienia działającym linkiem)
const VIDEO_SRC = \"https://www.w3schools.com/html/mov_bbb.mp4\"; 

function VideoPlayer() {
  const videoRef = useRef(null);

  const handlePlay = () => {
    if (videoRef.current) {
      videoRef.current.play()
        .catch(error => console.error(\"Błąd odtwarzania:\", error)); // Obsługa błędu, np. gdy użytkownik nie interagował jeszcze ze stroną
    } else {
        console.warn(\"Video ref not available\");
    }
  };

  const handlePause = () => {
    if (videoRef.current) {
      videoRef.current.pause();
    } else {
        console.warn(\"Video ref not available\");
    }
  };

  return (
    <div>
      <h2>Odtwarzacz Wideo</h2>
      <video 
        ref={videoRef} 
        src={VIDEO_SRC} 
        width=\"400\" 
        controls // Pokaż domyślne kontrolki dla porównania
      >
        Twoja przeglądarka nie obsługuje tagu video.
      </video>
      <div style={{ marginTop: \"10px\" }}>
        <button onClick={handlePlay}>Play</button>
        <button onClick={handlePause} style={{ marginLeft: \"5px\" }}>Pause</button>
      </div>
    </div>
  );
}

export default VideoPlayer;

// W App.jsx
import React from \"react\";
import VideoPlayer from \"./VideoPlayer\";

function App() {
  return (
    <div>
      <VideoPlayer />
    </div>
  );
}

export default App;

Cel: Stworzyć komponent odtwarzacza wideo, który używa useRef do uzyskania dostępu do elementu <video> i dodaje przyciski "Play" i "Pause".

Kroki:

  1. Stwórz komponent funkcyjny VideoPlayer.
  2. Użyj useRef do stworzenia referencji (np. videoRef) dla elementu <video>.
  3. Wyrenderuj element <video> z atrybutem ref={videoRef}. Dodaj atrybut src z linkiem do przykładowego pliku wideo (możesz użyć np. linku z sieci lub lokalnego pliku, jeśli masz skonfigurowany serwer). Dodaj atrybut controls, aby zobaczyć domyślne kontrolki.
  4. Dodaj dwa przyciski: "Play" i "Pause".
  5. Stwórz handlery handlePlay i handlePause dla tych przycisków.
  6. W handlerach użyj videoRef.current, aby wywołać odpowiednie metody na elemencie wideo: videoRef.current.play() i videoRef.current.pause(). Pamiętaj o sprawdzeniu, czy videoRef.current istnieje.
  7. Użyj komponentu VideoPlayer w App.jsx.

Zadanie do samodzielnego wykonania

Stwórz komponent ScrollToTopButton, który:

  1. Wyświetla przycisk "Do góry".
  2. Przycisk jest widoczny tylko wtedy, gdy użytkownik przewinął stronę w dół o określoną wartość (np. 200 pikseli). Użyj useState do przechowywania widoczności przycisku i useEffect do nasłuchiwania zdarzenia scroll na window.
  3. Gdy użytkownik kliknie przycisk, strona powinna płynnie przewinąć się do góry (window.scrollTo({ top: 0, behavior: \"smooth\" })).
  4. Użyj useRef do przechowywania ID timera, jeśli zdecydujesz się na debouncing/throttling dla handlera scrolla (opcjonalne, dla optymalizacji).

FAQ - Hook useRef

Czy mogę używać `useRef` do przechowywania stanu, który wpływa na renderowanie?

Nie powinno się. Jeśli zmiana wartości ma spowodować aktualizację UI, należy użyć `useState` lub `useReducer`. `useRef` jest przeznaczony do wartości, których zmiana *nie* powinna wywoływać ponownego renderowania.

Kiedy dokładnie `ref.current` jest ustawiany przy dostępie do DOM?

React ustawia wartość `ref.current` po zamontowaniu komponentu (po pierwszym renderowaniu) i zeruje ją tuż przed odmontowaniem. Dlatego bezpieczny dostęp do `ref.current` w celu interakcji z DOM zazwyczaj odbywa się wewnątrz `useEffect` lub handlerów zdarzeń.

Czy mogę przypisać ref do komponentu funkcyjnego?

Bezpośrednio nie. Jeśli spróbujesz zrobić `<MyFunctionComponent ref={myRef} />`, otrzymasz ostrzeżenie. Aby przekazać ref do komponentu funkcyjnego (np. w celu uzyskania dostępu do elementu DOM wewnątrz niego), komponent ten musi być opakowany w `React.forwardRef`.

Jaka jest różnica między `useRef` a tworzeniem obiektu `{ current: ... }` ręcznie?

`useRef` gwarantuje, że ten sam obiekt referencji jest zwracany przy każdym renderowaniu komponentu. Ręczne tworzenie obiektu `{ current: ... }` w ciele komponentu spowodowałoby tworzenie nowego obiektu przy każdym renderowaniu, tracąc trwałość wartości między renderowaniami.

Czy `useRef` jest tylko do DOM?

Nie, chociaż dostęp do DOM jest jego głównym zastosowaniem, `useRef` jest uniwersalnym narzędziem do przechowywania dowolnej mutowalnej wartości, która musi przetrwać między renderowaniami bez powodowania ich ponownego wywołania (np. ID timerów, poprzednie wartości stanu, instancje obiektów).

Czy zmiana `ref.current` w trakcie renderowania jest bezpieczna?

Technicznie możliwa, ale generalnie odradzana. Renderowanie powinno być czystym procesem. Modyfikowanie refów w trakcie renderowania może prowadzić do nieprzewidywalnych zachowań. Lepiej modyfikować refy wewnątrz `useEffect` lub handlerów zdarzeń.

Do czego służy `useImperativeHandle`?

`useImperativeHandle` jest używany w połączeniu z `forwardRef`. Pozwala komponentowi podrzędnemu dostosować, co dokładnie jest eksponowane rodzicowi przez ref. Zamiast udostępniać cały węzeł DOM, można udostępnić tylko wybrane metody (np. `focus`, `play`, `pause`), co jest lepsze dla enkapsulacji.