Przekazywanie referencji

Przekazywanie referencji (ang. ref forwarding) to technika, w której referencję do komponentu “podajemy dalej” do jego dziecka. Dla większości komponentów w aplikacji nie jest to potrzebne, jednak może okazać się przydatne w niektórych przypadkach, zwłaszcza w bibliotekach udostępniających uniwersalne komponenty. Najczęstsze scenariusze opisujemy poniżej.

Przekazywanie referencji do komponentów DOM

Rozważmy komponent FancyButton, który renderuje natywny element DOM - przycisk:

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

Komponenty reactowe ukrywają szczegóły swojej implementacji, w tym także wyrenderowany HTML. Inne komponenty używające FancyButton z reguły nie potrzebują mieć dostępu do referencji do wewnętrznego elementu button. Jest to korzystne, gdyż zapobiega sytuacji, w której komponenty są za bardzo uzależnione od struktury drzewa DOM innych komponentów.

Taka enkapsulacja jest pożądana na poziomie aplikacji, w komponentach takich jak FeedStory czy Comment. Natomiast może się okazać to niewygodne w przypadku komponentów wielokrotnego użytku, będących “liśćmi” drzewa. Np. FancyButton albo MyTextInput. Takie komponenty często używane są w wielu miejscach aplikacji, w podobny sposób jak zwyczajne elementy DOM typu button i input. W związku z tym, bezpośredni dostęp do ich DOM może okazać się konieczy, aby obsłużyć np. fokus, zaznaczenie czy animacje.

Przekazywanie referencji jest opcjonalną funkcjonalnością, która pozwala komponentom wziąć przekazaną do nich referencję i “podać ją dalej” do swojego dziecka.

W poniższym przykładzie FancyButton używa React.forwardRef, by przejąć przekazaną do niego referencję i przekazać ją dalej do elementu button, który renderuje:

const FancyButton = React.forwardRef((props, ref) => (  <button ref={ref} className="FancyButton">    {props.children}
  </button>
));

// Możesz teraz otrzymać bezpośrednią referencję do elementu „button”:
const ref = React.createRef();
<FancyButton ref={ref}>Kliknij mnie!</FancyButton>;

Tym sposobem komponenty używające FancyButton mają referencję do elementu button znajdującego się wewnątrz. Mogą więc, w razie potrzeby, operować na komponencie tak, jakby operowały bezpośrednio na natywnym elemencie DOM.

Oto wyjaśnienie krok po kroku, opisujące, co wydarzyło się w przykładzie powyżej:

  1. Tworzymy referencję reactową wywołując React.createRef i przypisujemy ją do stałej ref.
  2. Przekazujemy ref do <FancyButton ref={ref}> przypisując ją do atrybutu JSX.
  3. Wewnątrz forwardRef React przekazuje ref do funkcji (props, ref) => ... jako drugi argument.
  4. Podajemy argument ref dalej do <button ref={ref}> przypisując go do atrybutu JSX.
  5. Gdy referencja zostanie zamontowana, ref.current będzie wskazywać na element DOM <button>.

Uwaga

Drugi argument ref istnieje tylko, gdy definiujesz komponent przy pomocy wywołania React.forwardRef. Zwyczajna funkcja lub klasa nie dostanie argumentu ref, nawet jako jednej z właściwości (props).

Przekazywanie referencji nie jest ograniczone do elementów drzewa DOM. Możesz także przekazywać referencje do instancji komponentów klasowych.

Uwaga dla autorów bibliotek komponentów

Kiedy zaczniesz używać forwardRef w swojej bibliotece komponentów, potraktuj to jako zmianę krytyczną (ang. breaking change). W efekcie biblioteka powinna zostać wydana w nowej “wersji głównej” (ang. major version, major release). Należy tak postąpić, ponieważ najprawdopodobniej twoja biblioteka zauważalnie zmieniła zachowanie (np. inaczej przypinając referencje i eksportując inne typy). Może to popsuć działanie aplikacji, które są zależne od dawnego zachowania.

Stosowanie React.forwardRef warunkowo, gdy ono istnieje, także nie jest zalecane z tego samego powodu: zmienia to zachowanie biblioteki i może zepsuć działanie aplikacji użytkowników, gdy zmienią wersję Reacta.

Przekazywanie referencji w komponentach wyższego rzędu

Omawiana technika może okazać się wyjątkowo przydatna w komponentach wyższego rzędu (KWR; ang. Higher-Order Components lub HOC). Zacznijmy od przykładu KWR-a, który wypisuje w konsoli wszystkie właściwości komponentu:

function logProps(WrappedComponent) {  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('poprzednie właściwości:', prevProps);
      console.log('nowe właściwości:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;    }
  }

  return LogProps;
}

KWR logProps przekazuje wszystkie atrybuty do komponentu, który opakowuje, więc wyrenderowany wynik będzie taki sam. Na przykład, możemy użyć tego KWRa do logowania atrybutów, które zostaną przekazane do naszego komponentu FancyButton:

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Zamiast FancyButton eksportujemy LogProps.
// Jednak wyrenderowany zostanie FancyButton.
export default logProps(FancyButton);

Powyższe rozwiązanie ma jeden minus: referencje nie zostaną przekazane do komponentu. Dzieje się tak, ponieważ ref nie jest atrybutem. Tak jak key, jest on obsługiwany przez Reacta w inny sposób. Referencja będzie w tym wypadku odnosiła się do najbardziej zewnętrznego kontenera, a nie do opakowanego komponentu.

Oznacza to, że referencje przeznaczone dla naszego komponentu FancyButton będą w praktyce wskazywać na komponent LogProps.

import FancyButton from './FancyButton';

const ref = React.createRef();
// Komponent FancyButton, który zaimportowaliśmy, jest tak naprawdę KWR-em LogProps.
// Mimo że wyświetlony rezultat będzie taki sam,
// nasza referencja będzie wskazywała na LogProps zamiast na komponent FancyButton!
// Oznacza to, że nie możemy wywołać np. metody ref.current.focus()
<FancyButton
  label="Kliknij mnie"
  handleClick={handleClick}
  ref={ref}/>;

Na szczęście możemy jawnie przekazać referencję do wewnętrznego komponentu FancyButton używając API React.forwardRef. React.forwardRef przyjmuje funkcję renderującą, która otrzymuje parametry props oraz ref, a zwraca element reactowy. Na przykład:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('poprzednie właściwości:', prevProps);
      console.log('nowe właściwości:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;
      // 2. Przypiszmy nasz atrybut "forwardedRef" jako referencję
      return <Component ref={forwardedRef} {...rest} />;    }
  }

  // 1. Zwróć uwagę na drugi parametr "ref" dostarczony przez React.forwardRef.
  // Możemy go przekazać dalej do LogProps jako zwyczajny atrybut, np. "forwardedRef".
  // Następnie może on zostać przypisany do komponentu wewnątrz.
  return React.forwardRef((props, ref) => {    return <LogProps {...props} forwardedRef={ref} />;  });}

Wyświetlanie własnej nazwy w narzędziach deweloperskich

React.forwardRef przyjmuje funkcję renderującą. Narzędzia deweloperskie Reacta (ang. React DevTools) używają tej funkcji do określenia, jak wyświetlać komponent, który przekazuje referencję.

Przykładowo, następujący komponent w narzędziach deweloperskich wyświetli się jako ”ForwardRef“:

const WrappedComponent = React.forwardRef((props, ref) => {
  return <LogProps {...props} forwardedRef={ref} />;
});

Jeśli nazwiesz funkcję renderującą, narzędzia deweloperskie uwzględnią tę nazwę (np. ”ForwardRef(myFunction)”):

const WrappedComponent = React.forwardRef(
  function myFunction(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
);

Możesz nawet ustawić właściwość displayName funkcji tak, aby uwzględniała nazwę opakowanego komponentu:

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // Nadajmy temu komponentowi nazwę, która będzie bardziej czytelna w narzędziach deweloperskich.
  // np. "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;  forwardRef.displayName = `logProps(${name})`;
  return React.forwardRef(forwardRef);
}