No history yet

Mechanizm mutowalnych wartości

useRef: Pudełko na Twoje wartości

Zapewne kojarzysz hook useState jako podstawowy sposób na zarządzanie stanem, który powoduje ponowne renderowanie komponentu przy każdej zmianie. Ale co, jeśli potrzebujesz przechowywać jakąś wartość przez cały cykl życia komponentu, ale bez wywoływania renderowania za każdym razem, gdy ją aktualizujesz? Tu właśnie wkracza useRef.

Pomyśl o useRef jak o małym, trwałym pudełku. Możesz do niego włożyć dowolną wartość, a ona tam pozostanie między kolejnymi renderowaniami. Hook useRef zwraca obiekt, który zawsze ma jedną właściwość: current. To właśnie w ref.current przechowujesz swoje dane.

import { useRef } from 'react';

function MyComponent() {
  // Inicjalizujemy ref z wartością początkową 0
  const myRef = useRef(0);

  function handleClick() {
    // Bezpośrednio modyfikujemy wartość .current
    myRef.current = myRef.current + 1;
    console.log('Kliknięto! Wartość ref to:', myRef.current);
    // Ta zmiana NIE spowoduje ponownego renderowania komponentu!
  }

  console.log('Komponent się renderuje...');

  return <button onClick={handleClick}>Zwiększ ref</button>;
}

Synchronia a renderowanie

Najważniejsza różnica między useState a useRef dotyczy aktualizacji i renderowania. Kiedy wywołujesz funkcję ustawiającą stan (np. setCount(1)), React planuje ponowne renderowanie komponentu. Sama aktualizacja nie jest natychmiastowa. React przetwarza ją wsadowo, by zoptymalizować wydajność.

Zupełnie inaczej jest z useRef. Zmiana wartości ref.current jest i natychmiastowa. Kiedy piszesz myRef.current = 5, wartość jest zmieniana od razu, w tej samej chwili. Co kluczowe, ta operacja jest „niewidzialna” dla mechanizmu renderowania Reacta. Komponent nie dowiaduje się o zmianie i nie renderuje się ponownie.

Aktualizacja useState prosi React o ponowne renderowanie. Aktualizacja ref.current po prostu zmienia wartość.

Praktyczne zastosowania

useRef jest idealny do przechowywania danych, które nie są bezpośrednio związane z tym, co widzi użytkownik, ale są potrzebne do wewnętrznej logiki komponentu. Jednym z klasycznych przykładów jest przechowywanie identyfikatorów timerów.

import { useState, useRef, useEffect } from 'react';

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalIdRef = useRef(null);

  const handleStart = () => {
    // Przechowujemy ID interwału w ref, by mieć do niego dostęp później
    intervalIdRef.current = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);
  };

  const handleStop = () => {
    // Używamy ID z ref do zatrzymania interwału
    clearInterval(intervalIdRef.current);
  };

  useEffect(() => {
    // Oczyszczanie po odmontowaniu komponentu
    return () => clearInterval(intervalIdRef.current);
  }, []);

  return (
    <div>
      <p>Sekundy: {seconds}</p>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
    </div>
  );
}

Gdybyśmy użyli useState do przechowywania ID interwału, każda zmiana (start/stop) mogłaby potencjalnie wywołać niepotrzebne renderowanie. Z useRef unikamy tego problemu, a ID jest bezpiecznie przechowywane przez cały cykl życia komponentu.

Innym potężnym zastosowaniem jest śledzenie poprzedniej wartości propsa lub stanu. Ponieważ aktualizacje ref są synchroniczne, możemy przechować wartość, zanim zostanie ona zaktualizowana w następnym renderze.

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

  // useEffect uruchamia się PO renderowaniu
  useEffect(() => {
    // Aktualizujemy ref z bieżącą wartością, która stanie się "poprzednią" w następnym renderze
    prevValueRef.current = value;
  }); // Brak tablicy zależności oznacza, że uruchomi się po każdym renderowaniu

  const prevValue = prevValueRef.current;

  return (
    <p>
      Bieżąca wartość: {value}, Poprzednia wartość: {prevValue}
    </p>
  );
}

Wreszcie, useRef jest nieoceniony, gdy potrzebujesz dostępu do najnowszej wartości wewnątrz (closure), na przykład wewnątrz useEffect z pustą tablicą zależności. Stan (state) zamknięty w takim efekcie zawsze będzie miał wartość z pierwszego renderowania. Ref, jako że jest zewnętrznym, mutowalnym kontenerem, zawsze dostarczy aktualną wartość current.

Bezpieczna mutacja

W świecie Reacta często słyszy się: „nigdy nie mutuj stanu bezpośrednio”. To dlatego, że React polega na niezmienności (immutability), aby wykrywać zmiany i decydować o ponownym renderowaniu. Bezpośrednia mutacja obiektu stanu omija ten mechanizm i prowadzi do błędów.

Z useRef sytuacja jest inna. Cały sens istnienia tego hooka polega na dostarczeniu kontenera. React celowo nie śledzi zmian w ref.current. To daje nam swobodę bezpośredniej i synchronicznej modyfikacji tej wartości, ponieważ wiemy, że nie wpłynie to na proces renderowania. To bezpieczna i zamierzona przez twórców Reacta praktyka.

Kluczowa idea: Aktualizacja useState ponownie renderuje komponent. Aktualizacja useRef.current nie.

Używaj useRef, gdy potrzebujesz trwałego miejsca na dane, które nie wpływają bezpośrednio na wygląd UI, ale są kluczowe dla logiki komponentu.