Перенаправлення рефів

Перенаправлення рефів — це техніка для автоматичної передачі рефа від компонента до одного із його дітей. Для більшості компонентів, зазвичай, вона не є необхідною. Тим не менше, може бути корисною в деяких випадках, особливо якщо ви пишете бібліотеку. Давайте розглянемо найбільш поширені сценарії.

Перенаправлення рефів у DOM-компоненти

Розглянемо компонент FancyButton, який рендерить нативний DOM-елемент button:

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

React-компоненти приховують деталі своєї реалізації та результат рендеренгу. Також іншим компонентам, які використовують FancyButton, зазвичай не потрібен доступ до рефа його внутрішньому DOM-елементу button. І це добре тим, що це запобігає надмірній залежності компонентів від структури DOM-у один одного.

Хоча, така інкапсуляція є бажаною для компонентів, які описують певну закінчену частину додатка, наприклад, FeedStory або Comment, це може бути незручним для часто перевикористовуваних “дрібних” компонентів, таких як FancyButton та MyTextInput. Ці компоненти використовуються в додатку подібно до звичайних DOM button чи input і доступ до їхніх DOM-вузлів може бути необхідним для управління фокусом, виділенням або анімацією.

Перенаправлення рефів дає можливість певному компоненту взяти отриманий реф і передати його далі (іншими словами “перенаправити”) до дочірнього компонента.

У прикладі нижче, FancyButton використовує React.forwardRef, щоб отримати переданий йому ref і перенаправити його в DOM button, який він рендерить:

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

// Тепер ви можете отримати реф беспосередньо на DOM button
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

Таким чином, компоненти, що використовують FancyButton можуть отримати реф внутрішнього DOM-вузла button і якщо потрібно, мати доступ до DOM button подібно до того, якби він використовувався напряму.

Розглянемо цей приклад покроково:

  1. Ми створюємо React-реф, викликаючи React.createRef і записуєм його у змінну ref.
  2. Ми передаємо наш ref у <FancyButton ref={ref}>, вказуючи його як JSX-атрибут.
  3. React передає ref у функцію (props, ref) => ... всередині forwardRef другим аргументом.
  4. Ми перенаправляєм аргумент ref далі у <button ref={ref}>, вказуючи його як JSX-атрибут.
  5. Після прив’язки рефа, ref.current буде вказувати на DOM-вузол <button>.

Примітка

Другий аргумент ref існує тільки тоді, коли ви визначаєте компонент як виклик функції React.forwardRef. Звичайні функціональні або класові компоненти не отримують ref у якості аргумента чи пропса.

Перенаправлення рефів не обмежуються DOM-компонентами. Ви також можете перенаправити реф у екземпляр класового компонента.

Примітка для розробників бібліотек компонентів

Коли ви починаєте використовувати forwardRef у бібліотеці компонентів, ви повинні вважати це як несумісну зміну і випустити нову мажорну версію. Причина цього в тому, що, швидше за все, компонент буде мати замітно іншу поведінку (наприклад: зміниться тип експортованих даних і елемент, до якого прив’язаний реф), в результаті чого додатки та інші бібліотеки, що покладаються на стару поведінку, перестануть працювати.

З цієї ж причини ми не рекомендуємо викликати React.forwardRef умовно (тобто перевіряючи чи функція існує). Це змінить поведінку вашої бібліотеки і додатки ваших користувачів можуть перестати працювати при оновленні версії React.

Перенаправлення рефів у компоненти вищого порядку

Ця техніка також може бути особливо корисною у компонентах вищого порядку (ще відомі, як КВП або англ. — HOC). Давайте почнемо з прикладу КВП, який виводить пропси компонента в консоль:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

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

  return LogProps;
}

КВП “logProps” передає всі props до компонента, який він обгортає, так що результат рендеру буде такий самий. Наприклад, ми можемо використати цей КВП, щоб вивести усі пропси передані в наш компонент FancyButton:

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

  // ...
}

// Замість того, щоб експортувати FancyButton, ми експортуємо LogProps.
// При цьому рендеритись буде FancyButton.
export default logProps(FancyButton);

Щодо прикладу вище є одне застереження: тут рефи не будуть передаватись. Це тому, що ref не є пропом. React опрацьовує ref по-іншому, подібно до key. Якщо ви додасте реф до КВП, реф буде вказувати на зовнішній компонент-контейнер, а не на обгорнутий компонент.

Це означає, що рефи призначені для компонента FancyButton насправді будуть прив’язані до компонента LogProps:

import FancyButton from './FancyButton';

const ref = React.createRef();

// Компонент FancyButton, який ми імпортуємо — це КВП LogProps.
// Навіть, якщо результат рендерингу буде таким же самим,
// Наш реф буде вказувати на LogProps, а не на внутрішній компонент FancyButton!
// Це означає, що ми, наприклад, не можемо викликати ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

На щастя, ми можемо явно перенаправити реф до внутрішнього компонента FancyButton, використовуючи React.forwardRef API. React.forwardRef приймає функцію рендеринга, яка отримує параметри props і ref та повертає React-вузол. Наприклад:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // Передаємо в якості рефа проп "forwardedRef"
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Зауважте, другий параметр "ref" переданий від React.forwardRef.
  // Ми можемо передати його в LogProps, як звичайний проп, наприклад: "forwardedRef",
  // А потім прив'язати його до компоненту.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

Відображення іншого імені в DevTools

React.forwardRef отримує фукнцію рендерингу. React DevTools використовує цю функцію, щоб визначити, як відображати компонент перенаправлення рефа.

Наприклад, наступний компонент буде відображатись в DevTools, як ”ForwardRef“:

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

Якщо надати ім’я функції рендеринга, то воно з’явиться у назві компонента в DevTools (наприклад, ”ForwardRef(myFunction)”):

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

Ви можете навіть додати властивість до функції displayName і вказати в ній, який саме компонент обгорнутий.

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

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

  // Дамо цьому компоненту більш зрозуміле ім'я в DevTools
  // Наприклад, "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}