Poznajemy drugą część hooka useRef, czyli zastosowanie do manipulowania drzewem DOM w React oraz forwardRef. Do dzieła.

Tworzymy ref z nullem:

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  // ...

Gdzieś w JSX mamy element z atrybutem ref równym naszej zmiennej:

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

W event handlerach wykorzystujemy ref.current, aby manipulować elementem:

function handleClick() {
    inputRef.current.focus();
  }

Przykład pierwszy, focus elementu na klik:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Przykład drugi ze scrollem, pokażemy od końca, czyli najpierw JSX:

return (
    <>
      <nav>
        <button onClick={() => scrollToIndex(0)}>
          Tom
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Maru
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
            />
          </li>
        </ul>
      </div>
    </>
  );
}

Mamy tutaj delegację (swego rodzaju) refów, ref jest na ul ustawione. Dobra, teraz JS:

import { useRef } from 'react';

export default function CatFriends() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    // This line assumes a particular DOM structure:
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

Jak widać normalny querySelectorAll na tym wywołujemy, indeks odpowiedni mamy z onClick, inaczej musielibyśmy tworzyć ref do każdego jednego img, tego raczej nie chcemy.

Odtwarzacz video też pokażemy sobie najpierw od strony JSX:

return (
    <>
      <button onClick={handleClick}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <video
        width="250"
        ref={ref}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
      >
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  );
}

A teraz JS:

import { useState, useRef } from 'react';

export default function VideoPlayer() {
  const [isPlaying, setIsPlaying] = useState(false);
  const ref = useRef(null);

  function handleClick() {
    const nextIsPlaying = !isPlaying;
    setIsPlaying(nextIsPlaying);

    if (nextIsPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }

Co tu się dzieje na klik? Zakładam, że mamy stan isPlaying na false:

  • Tworzymy zmienną nextIsPlaying i ustawiamy na przeciwieństwo, czyli true
  • Wywołujemy zmianę stanu na tę zmienną, czyli ustawiamy stan na true
  • Jeszcze nie mamy re-rendera. Właśnie po to nam nextIsPlaying, stan się zmieni jak skończy się funkcja handleClick
  • Jeszcze stan jest na false, choć true ustawiliśmy, ale mamy nextIsPlaying
  • W zależności od jego wartości używamy refa aby odpalić albo zatrzymać odtwarzanie

No i jeszcze forwardRef, czyli jak tworzyć własne komponenty, do których komponenty-rodzice mogą robić ref:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Dokumentacja ostrzega – choć initial state jest używany tylko do pierwszego rendera, każdy inny jest wyrzucany, to jednak funkcje się wywołują:

function Video() {
  const playerRef = useRef(new VideoPlayer());
  // ...

Po prostu odrzucane są, ale wywołują się co render. Swoją drogą nie wiem, dlaczego nie można tam przekazać funkcji strzałkowej i czy by to rozwiązało problem, ale dokumentacja pokazuje taki sposób:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }

Zawsze mi się wydawało, że w React unikamy takich rzeczy poprzez podawanie funkcji strzałkowej zwracającej np. initial state zamiast tego newVideoPlayer, ale trzymamy się standardów nakreślonych w oficjalnej dokumentacji, oni się powinni znać na rzeczy.

Podają też ciekawy przykład funkcji na potem:

function Video() {
  const playerRef = useRef(null);

  function getPlayer() {
    if (playerRef.current !== null) {
      return playerRef.current;
    }
    const player = new VideoPlayer();
    playerRef.current = player;
    return player;
  }

Więcej Reacta niedługo…