Odwoływanie się do wartości za pomocą referencji

Gdy chcesz, aby komponent “zapamiętał” pewne informacje, ale nie chcesz, aby te informacje wywoływały ponowne renderowania, możesz użyć referencji (ang. reference, w skrócie ref).

W tej sekcji dowiesz się

  • Jak dodać referencję do komponentu
  • Jak zaktualizować wartość referencji
  • Czym referencje różnią się od stanu
  • Jak bezpiecznie używać referencji

Dodawanie referencji do komponentu

Możesz dodać referencję do swojego komponentu, importując hook useRef z Reacta:

import { useRef } from 'react';

We wnętrzu swojego komponentu wywołaj hook useRef, przekazując jako jedyny argument wartość początkową, do której chcesz się odwoływać. Na przykład, oto referencja do wartości 0:

const ref = useRef(0);

useRef zwraca obiekt o następującej strukturze:

{
current: 0 // Wartość, którą przekazałeś do useRef
}
Strzałka z napisem 'current' włożona do kieszeni z napisem 'ref'

Autor ilustracji Rachel Lee Nabors

Możesz uzyskać dostęp do bieżącej wartości tej referencji przez właściwość ref.current. Ta wartość jest celowo mutowalna (ang. mutable), co oznacza, że możesz ją zarówno odczytywać, jak i zapisywać. To jak taka ukryta kieszeń twojego komponentu, której React nie śledzi. To właśnie sprawia, że jest to “ukryta furtka” pozwalająca wyjść poza jednokierunkowy przepływ danych w Reakcie - więcej na ten temat poniżej!

Tutaj, przycisk będzie inkrementować ref.current przy każdym kliknięciu:

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('Kliknięto ' + ref.current + ' razy!');
  }

  return (
    <button onClick={handleClick}>
      Kliknij mnie!
    </button>
  );
}

Referencja wskazuje na liczbę, ale podobnie jak stan, możesz wskazać na cokolwiek: ciąg znaków, obiekt, a nawet funkcję. W przeciwieństwie do stanu, referencja to zwykły obiekt javascriptowy z właściwością current, którą możesz odczytać i modyfikować.

Zwróć uwagę, że komponent nie renderuje się ponownie przy każdej inkrementacji. Podobnie jak stan, referencje są przechowywane przez Reacta między przerenderowaniami. Jednak ustawienie stanu powoduje ponowne renderowanie komponentu, a zmiana referencji - nie!

Przykład: budowanie stopera

Możesz połączyć referencje i stan w jednym komponencie. Na przykład zbudujmy stoper, który użytkownik może uruchomić lub zatrzymać, naciskając przycisk. Aby wyświetlić, ile czasu minęło od momentu naciśnięcia przycisku “Start”, musisz śledzić, kiedy przycisk Start został naciśnięty i jaki jest bieżący czas. Te informacje są używane do renderowania, dlatego będziesz przechowywać je w stanie:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

Kiedy użytkownik naciśnie “Start”, użyjesz metody setInterval, aby aktualizować czas co 10 milisekund:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Rozpoczęcie odliczania.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Zaktualizowanie bieżącego czasu co 10 ms.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Minęło: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

Kiedy przycisk “Stop” zostanie naciśnięty, musisz anulować istniejący interwał, aby przestał aktualizować zmienną stanu now. Możesz to zrobić, wywołując metodę clearInterval, ale musisz przekazać mu identyfikator interwału, który został wcześniej zwrócony przez wywołanie metody setInterval po naciśnięciu przycisku Start. Musisz przechować identyfikator interwału w jakimś miejscu. Ponieważ identyfikator interwału nie jest używany do renderowania, możesz przechować go w referencji:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Minęło: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

Jeśli dana informacja jest wykorzystywana podczas renderowania, przechowuj ją w stanie. Kiedy informacja jest potrzebna tylko dla procedur obsługi zdarzeń i jej zmiana nie wymaga ponownego renderowania, użycie referencji może być bardziej wydajne.

Różnice między referencjami a stanem

Możesz pomyśleć, że referencje wydają się mniej “rygorystyczne” niż stan – na przykład pozwalają na mutowanie wartości bez konieczności używania funkcji do ustawiania stanu. Jednak w większości przypadków będziesz chcieć używać stanu. Referencje to “ukryte furtki”, które rzadko będziesz wykorzystywać. Oto porównanie stanu i referencji:

refsstate
useRef(initialValue) zwraca { current: initialValue }useState(initialValue) zwraca bieżącą wartość zmiennej stanu oraz funkcję ustawiającą stan ([value, setValue])
Nie wywołuje ponownego renderowania, gdy ją zmienisz.Wywołuje ponowne renderowanie, gdy go zmienisz.
Mutowalny - możesz modyfikować i aktualizować wartość current poza procesem renderowania.Niemutowalny - musisz używać funkcji ustawiającej stan, aby modyfikować zmienne stanu i zainicjować kolejne renderowanie.
Nie powinno się odczytywać (ani zapisywać) wartości current podczas renderowania.Możesz odczytać stan w dowolnym momencie. Jednak każde renderowanie ma swoją własną migawkę stanu, która nie zmienia się.

Oto przycisk licznika zaimplementowany za pomocą stanu:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Kliknięto {count} razy
    </button>
  );
}

Ponieważ wartość count jest wyświetlana, ma sens użycie zmiennej stanu do jej przechowywania. Kiedy wartość licznika jest ustawiana za pomocą setCount(), React ponownie renderuje komponent, a ekran jest aktualizowany, aby odzwierciedlić nową wartość licznika.

Gdyby spróbować zaimplementować to za pomocą referencji, React nigdy nie przerenderowałby ponownie komponentu, więc zmiana licznika nie byłaby nigdy widoczna! Zauważ, że kliknięcie tego przycisku nie aktualizuje jego tekstu:

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // To nie przerenderowuje komponentu!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      Kliknięto {countRef.current} razy
    </button>
  );
}

Dlatego odczytywanie ref.current podczas renderowania prowadzi do niepewnego kodu. Jeśli tego potrzebujesz, użyj stanu zamiast referencji.

Dla dociekliwych

Jak działa useRef wewnątrz??

Chociaż zarówno useState, jak i useRef są udostępniane przez Reacta, w zasadzie useRef mogłoby być zaimplementowane na bazie useState. Możesz sobie wyobrazić, że wewnątrz Reacta, useRef jest zaimplementowane w ten sposób:

// Wewnątrz Reacta
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

Podczas pierwszego renderowania, useRef zwraca { current: initialValue }. Ten obiekt jest przechowywany przez Reacta, więc podczas następnego renderowania zostanie zwrócony ten sam obiekt. Zauważ, że w tym przykładzie funkcja ustawiająca stan nie jest używana. Jest to zbędne, ponieważ useRef zawsze musi zwracać ten sam obiekt!

React zapewnia wbudowaną wersję useRef, ponieważ jest to na tyle powszechne w użyciu. Możesz jednak traktować to jako zwykłą zmienną stanu bez funkcji ustawiającej. Jeśli znasz programowanie obiektowe, referencje mogą przypominać ci pola instancji, ale zamiast this.something napiszesz somethingRef.current.

Kiedy używać referencji

Zwykle będziesz używać referencji, gdy twój komponent będzie musiał “wyjść poza” Reacta i komunikować się z zewnętrznymi API, najczęściej z API przeglądarki, które nie wpływa na wygląd komponentu. Oto kilka tych rzadkich przypadków użycia:

Jeśli twój komponent musi przechować wartość, która nie wpływa na logikę renderowania, użyj referencji.

Najlepsze praktyki dotyczące referencji

Stosowanie się do tych zasad sprawi, że twoje komponenty będą bardziej przewidywalne:

  • Traktuj referencje jako ukrytą furtkę. Referencje przydają się podczas pracy z systemami zewnętrznymi lub API przeglądarki. Jeśli większość logiki aplikacji i przepływu danych opiera się na referencjach, warto przemyśleć swoje podejście.
  • Nie odczytuj ani nie zapisuj ref.current podczas renderowania. Jeśli jakaś informacja jest potrzebna podczas renderowania, użyj stanu. Ponieważ React nie wie, kiedy ref.current się zmienia, nawet odczytanie go podczas renderowania sprawia, że zachowanie komponentu staje się trudne do przewidzenia. Jedynym wyjątkiem od tej zasady jest kod taki jak if (!ref.current) ref.current = new Thing(), który ustawia referencję tylko raz podczas pierwszego renderowania.

Ograniczenia stanu w React nie dotyczą referencji. Na przykład, stan działa jak migawka dla każdego renderowania i nie aktualizuje się synchronicznie. Jednak gdy zmieniasz bieżącą wartość referencji, zmiana następuje natychmiastowo:

ref.current = 5;
console.log(ref.current); // 5

Dzieje się tak, ponieważ referencja sama w sobie jest zwykłym obiektem javascriptowym, więc tak też się zachowuje.

Nie musisz również martwić się o unikanie mutacji podczas pracy z referencją. Dopóki obiekt, który mutujesz, nie jest używany do renderowania, React nie interesuje się tym, co robisz z referencją lub jej zawartością.

Referencje i DOM

Referencję możesz przypisać do dowolnej wartości. Jednak najczęstszym przypadkiem użycia referencji jest dostęp do elementu DOM. Jest to przydatne, jeśli na przykład chcesz programowo ustawić fokus na polu wejściowym. Jeśli przekażesz referencję do atrybutu ref w JSX, jak na przykład <div ref={myRef}>, React umieści odpowiadający element DOM w myRef.current. Gdy element zostanie usunięty z DOM, React zaktualizuje myRef.current, ustawiając go na null. Możesz przeczytać więcej na ten temat w rozdziale Manipulowanie DOM przy użyciu referencji.

Powtórka

  • Referencje to “ukryte furtki” do przechowywania wartości, które nie są używane do renderowania. Nie będziesz ich potrzebować zbyt często.
  • Referencja to zwykły obiekt javascriptowy z pojedynczą właściwością o nazwie current, którą możesz odczytać lub ustawić.
  • Możesz uzyskać referencję od Reacta, wywołując hook useRef.
  • Podobnie jak stan, referencje pozwalają przechowywać informacje między przerenderowaniami komponentu.
  • W przeciwieństwie do stanu, ustawienie wartości current referencji nie powoduje ponownego przerenderowania.
  • Nie odczytuj ani nie zapisuj ref.current podczas renderowania. To sprawia, że działanie komponentu staje się trudne do przewidzenia.

Wyzwanie 1 z 4:
Napraw uszkodzone pole wprowadzania czatu

Wpisz wiadomość i kliknij “Wyślij”. Zauważysz, że pojawi się opóźnienie trzech sekund, zanim zobaczysz komunikat “Wysłano!“. W tym czasie możesz zobaczyć przycisk “Cofnij”. Kliknij go. Przycisk “Cofnij” ma zatrzymać wyświetlanie komunikatu “Wysłano!“. Robi to poprzez wywołanie metody clearTimeout dla identyfikatora timeout zapisanego podczas handleSend. Jednak nawet po kliknięciu “Cofnij” komunikat “Wysłano!” nadal się pojawia. Znajdź przyczynę, dlaczego to nie działa i napraw to.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Wysłano!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Wysyłanie...' : 'Wyślij'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Cofnij 
        </button>
      }
    </>
  );
}