Skoro znamy już useRef i forwardRef, najwyższa pora poznać hook useImperativeHandle. Do dzieła.

Ok, najpierw przypomnijmy sobie forwardRef. Komponent-dziecko, komponent forwardujący:

import { forwardRef } from 'react';

const VideoPlayer = forwardRef(function VideoPlayer({ src, type, width }, ref) {
  return (
    <video width={width} ref={ref}>
      <source
        src={src}
        type={type}
      />
    </video>
  );
});

export default VideoPlayer;

Komponent forwardujący ref nie tworzy (nie korzysta z useRef) tylko je od rodzica przyjmuje i jako komponent, czyli nieelement, forwarduje do swojego elementu.

Rodzic wygląda tak:

import { useRef } from 'react';
import MyVideoPlayer from './MyVideoPlayer.js';

export default function App() {
  const ref = useRef(null);
  return (
    <>
      <button onClick={() => ref.current.play()}>
        Play
      </button>
      <button onClick={() => ref.current.pause()}>
        Pause
      </button>
      <br />
      <MyVideoPlayer
        ref={ref}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
        type="video/mp4"
        width="250"
      />
    </>
  );
}

Czyli rodzic:

  • Korzysta z useRef
  • Tworzy ref do nieelementu RFC MyVideoPlayer, który forwarduje ref do elementu video
  • Ma pełen dostęp do wszystkiego, całego HTMLVideoElement z komponentu MyVideoPlayer

Nie zawsze tego chcemy. I tu przychodzi z pomocą useImperativeHandle, aczkolwiek pamiętać należy, że tutaj useRef będzie używane zarówno przez rodzica jak i dziecko forwardujące ref do elementu.

I wykorzystywać to będziemy, aby mieć kontrolę co rodzic-komponent może a co nie może robić z refem forwardowanym przez dziecko do elementu.

Ok, komponent-dziecko:

mport { forwardRef, useRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus();
      },
      scrollIntoView() {
        inputRef.current.scrollIntoView();
      },
    };
  }, []);

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

export default MyInput;

Czyli tak:

  • Mamy forwardRef, za propsami RFC przyjmuje ref
  • Nie wystarczy do elementu dodać atrybut ref i ustawić na ref przyjmowany od rodzica!
  • Trzeba użyć useRef, stworzyć własny ref, ustawiamy na null
  • Trzeba skorzystać z useimperativehandle, który przyjmie ref od rodzica i callback
  • W callback dajemy obiekt zawierający metody, a w tych metodach pracujemy nad naszym refem (nie od rodzica, tylko przez nas utworzonym)
  • To sprawia, że rodzic może utworzyć ref, przekazać do naszego RFC, zaś to RFC forwarduje ten ref, ale do useImperativeHandle, które pozwala tylko na niektóre metody dotyczące inputRef, zatem tylko to można na tym elemencie robić, co pozwolimy
  • No i potrzebna jest tablica zależności, znamy ją z innych hooków, więc chyba nie ma co jej tłumaczyć

Teraz jak wygląda rodzic:

import { useRef } from 'react';
import MyInput from './MyInput.js';

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

  function handleClick() {
    ref.current.focus();
    // This won't work because the DOM node isn't exposed:
    // ref.current.style.opacity = 0.5;
  }

  return (
    <form>
      <MyInput placeholder="Enter your name" ref={ref} />
      <button type="button" onClick={handleClick}>
        Edit
      </button>
    </form>
  );
}

Rodzic:

  • Też tworzy ref
  • Dla nieelementu RFC ustawia atrybut ref na ten ref
  • Nieelement RFC forwarduje ten ref nie do swojego elementu, ale do useImperativeHandle
  • UseImperativeHandle zwraca obiekt z metodami różnymi
  • Te metody operują na wewnętrznym ref utworzonym przez dziecko
  • Ta cała zmyślna logika zapewnia enkapsulację i to, że komponent rodzic może zrobić z elementem input tylko to, na co mu pozwalamy, nie może na przykład usawić my atrybutu style