3.16 Передача ссылок


Передача ссылки - это метод для автоматической передачи ref через компонент к одному из его потомков. Обычно это не требуется для большинства компонентов приложения. Однако это может быть полезно для их некоторых видов, особенно в библиотеках компонентов для многоразового использования. Наиболее распространенные сценарии описаны ниже.





Рассмотрим компонент FancyButton, который отображает нативный DOM-элемент button:


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

Компоненты React скрывают свои детали реализации, включая результат отрисовки. Другие компоненты, использующие FancyButton, обычно не нуждаются в получении ссылки ref на внутренний DOM-элемент button. Это хорошо, так как мешает компонентам слишком сильно полагаться на DOM-структуру друг друга.

Хотя такая инкапсуляция и желательна для компонентов уровня приложения, таких как FeedStory или Comment, она может оказаться неудобной для часто переиспользуемых «листовых» компонентов(листья дерева компонентов), таких как FancyButton или MyTextInput. Эти компоненты, как правило, используются во всем приложении аналогичным образом, как и обычные DOM-компоненты button и input, а доступ к их DOM-узлам может быть неизбежным для управления фокусировкой, выбором(тег select) или анимацией.

Передача ссылок (ref forwarding) - это дополнительная функция, позволяющая компонентам передавать получаемую ссылку ref дальше своему потомку.

В приведенном ниже примере компонент FancyButton использует React.forwardRef для получения переданной ему ссылки ref и последующей передачи её DOM кнопке button, которую он отображает:


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

  // Теперь вы можете получить ссылку ref напрямую и передать ее DOM кнопке button:
  const myRef = React.createRef();
  <FancyButton ref={myRef}>Click me!</FancyButton>;
  

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

Приведем пошаговое объяснение того, что происходит в приведенном выше примере:

  • Мы создаем React ссылку вызывая React.createRef и присваиваем её переменной myRef.
  • Далее передаем нашу ссылку ниже в <FancyButton ref={myRef}>, указывая её как JSX атрибут ref.
  • React передает ссылку в функцию (props, ref) => ... внутри forwardRef вторым аргументом.
  • Мы передаем данный аргумент ref ниже в <button ref={ref}>, указывая его как JSX атрибут ref.
  • Когда ссылка будет присоединена, ref.current будет указывать на DOM-узел <button>.


Внимание!

Второй аргумент ref передается только при определении компонента с помощью вызова React.forwardRef. Обычные компоненты-функции или компоненты-классы не получают аргумент ref. Он также недоступен и в свойствах props.

Передача ссылок не ограничивается DOM-компонентами. Вы также можете передавать ссылки экземплярам компонентов-классов.




Когда вы начнете использовать forwardRef в библиотеке компонентов, вы должны рассматривать это как ломающее изменение и выпускать новую major версию вашей библиотеки. Это связано с тем, что ваша библиотека, вероятно, будет иметь отличающееся поведение (например, из-за того какие ссылки присваиваются и какие типы экспортируются). Это может нарушить работу существующих приложений и других библиотек, которые зависят от вашего старого поведения.



Условное применение React.forwardRef, если оно существует, также не рекомендуется по тем же причинам: оно меняет поведение вашей библиотеки и может нарушать работу приложений ваших пользователей при обновлении самого React.




Также данный подход может быть особенно полезен для компонентов более высокого порядка (также известных как HOC). Начнем с примера HOC, который логирует свойства props компонента в консоли:


Код
    
  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 компоненту, который он оборачивает, поэтому отображаемый результат не изменится. Например, мы можем использовать этот HOC для логирования всех свойств, которые передаются нашему компоненту «fancy button»:


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

    // ...
  }

  // Вместо экспортирования FancyButton, мы экспортируем LogProps.
  // Он по-прежнему будет отрисовывать FancyButton.
  export default logProps(FancyButton);
  

В приведенном выше примере есть один подводный камень: ссылки ref передаваться не будут. Это происходит потому, что ref не является свойством. Подобно ключу key, ссылка ref обрабатывается React-ом по-другому. Если вы добавите ссылку ref в HOC, она будет ссылаться на самый внешний компонент-контейнер, а не на обернутый компонент.

Это означает, что ссылки, предназначенные для нашего компонента FancyButton, будут на самом деле привязаны к компоненту LogProps:


Код
    
  import FancyButton from './FancyButton';
  
  const ref = React.createRef();

  // Компонент FancyButton, который мы импортировали, является
  // старшим компонентом LogProps. Даже если отрисованный результат
  // будет прежним, наша ссылка ref будет указывать на LogProps,
  // вместо вложенного компонента FancyButton! Это означает, что мы не
  // можем вызвать, например ref.current.focus()
  <FancyButton
          label="Click Me"
          handleClick={handleClick}
          ref={ref}
  />;
  

К счастью, мы можем явно передать ссылки на внутренний компонент FancyButton, используя API React.forwardRef. React.forwardRef принимает render-функцию (функцию отрисовки), которая получает параметры 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" как ссылку ref
          return <Component ref={forwardedRef} {...rest} />;
        }
      }

      // Обратите внимание: второй параметр "ref" предоставлен React.forwardRef.
      // Мы можем передать его дальше в LogProps как обычное свойство, например "forwardedRef"
      // Затем ссылка может быть присоединена к Component.
      return React.forwardRef((props, ref) => {
         return <LogProps {...props} forwardedRef={ref} />;
      });
    }
  




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

Например, следующий компонент появится как «ForwardRef» в DevTools:


Код
    
  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} />;
    }

    // Дайте этому компоненту полезное отображаемое имя name в DevTools.
    // например "ForwardRef(logProps(MyComponent))"
    const name = Component.displayName || Component.name;
    forwardRef.displayName = `logProps(${name})`;
  
    return React.forwardRef(forwardRef);
  }