React. Продвинутые руководства. Часть Третья


Продолжение серии переводов раздела "Продвинутые руководства" (Advanced Guides) официальной документации библиотеки React.js.


Ref-атрибуты и DOM в React


В типовом потоке данных React, свойства (props) — это единственный способ, с помощью которого родители взаимодействуют со своими потомками. Для модификации потомка, вам необходимо заново отобразить (произвести ререндеринг) его с новыми свойствами. Однако, в некоторых случаях, вам понадобится модифицировать потомка непосредственно, вне основного потока. Изменение потомка возможно в случаях если он является экземпляром компонента React или элементом DOM. Для обоих этих случаев React имеет особый способ изменения.


Атрибут обратного вызова ref


React поддерживает специальный атрибут, который может быть присвоен любому компоненту. Атрибут ref принимает функцию обратного вызова, и вызывает ее после того, как компонент монтируется в DOM или удаляется из него.


Когда атрибут ref используется в элементе HTML, функция обратного вызова принимает базовый элемент DOM в качестве аргумента. Например, следующий код использует функцию обратного вызова, указанную в ref, для сохранения ссылки на узел DOM:


class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.focus = this.focus.bind(this);
  }

  focus() {
    // Установка фокуса на поле текстового ввода (input) с явным использованием исходного API DOM
    this.textInput.focus();
  }

  render() {
    // Использование обратного вызова `ref` для сохранения ссылки на поле текстового ввода (input)
    // как элемента DOM в this.textInput.
    return (
      <div>
        <input
          type="text"
          ref={(input) => { this.textInput = input; }} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focus}
        />
      </div>
    );
  }
}

React вызывает функцию обратного вызова ref с элементом DOM в качестве аргумента когда компонент монтируется, и со значением null в качестве аргумента когда компонент удаляется.


Использование обратного вызова ref для установки свойства в классе — это общепринятый шаблон для доступа к элементам DOM. Предпочтительный способ для установки свойства с использованием обратного вызова ref — тот который приведен в примере выше. Есть еще более короткий способ для реализации этого: ref={input => this.textInput = input}.


Если вы работали ранее с React, вы можете быть знакомы со старой версией API, когда атрибут ref является строкой, например, таким как "textInput" и узел DOM доступен как this.refs.textInput. Мы не рекомендуем пользоваться этим, т.к. со строчными ref есть некоторые проблемы, мы считаем их устаревшими и, возможно, они будут удалены в будущих версиях. Если в настоящий момент вы используете this.refs.myRefName, мы рекомендуем перейти к использованию описанного нами шаблона.


Когда атрибут ref используется в кастомном компоненте React, функция обратного вызова принимает смонтированный экземпляр компонента в качестве аргумента. Например, если мы захотели обернуть input из предыдущего примера в компонент CustomTextInput для симуляции клика сразу после монтирования:


class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.focus = this.focus.bind(this);
  }

  focus() {
    // Установка фокуса на поле текстового ввода (input) с явным использованием исходного API DOM
    this.textInput.focus();
  }

  render() {
    // Использование обратного вызова `ref` для сохранения ссылки на поле текстового ввода (input)
    // как элемента DOM в this.textInput.
    return (
      <div>
        <input
          type="text"
          ref={(input) => { this.textInput = input; }} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focus}
        />
      </div>
    );
  }
}

class AutoFocusTextInput extends React.Component {
  componentDidMount() {
    this.textInput.focus();
  }

  render() {
    return (
      <CustomTextInput
        ref={(input) => { this.textInput = input; }} />
    );
  }
}

Нельзя использовать атрибут ref с компонентом, построенным на функции (stateless компонент), т.к. функция не имеет экземпляров. Однако, вы можете использовать атрибут ref внутри такого компонента:


function CustomTextInput(props) {
  // textInput задекларирован здесь, т.к. обратный вызов ref ссылается на него
  let textInput = null;

  function handleClick() {
    textInput.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={(input) => { textInput = input; }} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );  
}

Не злоупотребляйте обратными вызовами ref


Вашей первой мыслью может быть — что использование ref "превратит мечту в реальность" в вашем приложении. Если это так, то остановитесь и критически подумайте — верно ли расположены состояния в вашей иерархии компонентов. Часто возникает такая ситуация, что перерасположение состояния выше в иерархии компонентов, чем оно находится в настоящий момент, решает проблему. Смотрите руководство Подъем Состояния выше как пример этого.


Следующие части:



Предыдущие части:



Первоисточник: React — Advanced Guides — Refs and the DOM

Поделиться с друзьями
-->

Комментарии (24)


  1. TheShock
    14.01.2017 03:19
    +3

    Как альтернатива — можно использовать строковое название и тогда не придется каждый вызов создавать анонимную функцию, не знаю, почему в статье нету этого примера.

    class Hello extends React.Component {
      constructor() {
        this.focus = this.focus.bind(this);
      }
    
      handleClick() {
        this.refs.myTextInput.focus();
      }
    
      render() {
        return (
          <div>
            <input type="text" ref="myTextInput" />
            <input type="button" value="Focus" onClick={this.focus} />
          </div>
        );
      }
    }
    


    edit: таки описано:
    Использование обратного вызова ref для установки свойства в классе — это общепринятый шаблон для доступа к элементам DOM. Если в настоящий момент для этой задачи вы используете this.refs.myRefName, мы рекомендуем перейти к использованию описанного нами шаблона.


    не хватает аргументов, почему рекомендуют.


    1. vtikunov
      14.01.2017 03:22

      В статье об этом написано:

      Использование обратного вызова ref для установки свойства в классе — это общепринятый шаблон для доступа к элементам DOM. Если в настоящий момент для этой задачи вы используете this.refs.myRefName, мы рекомендуем перейти к использованию описанного нами шаблона.


    1. vtikunov
      14.01.2017 03:28
      +1

      Не надо давать уникальные имена универсальным шаблонным элементам, можно использовать универсальные функции. Представьте, что у вас 2 inputa и 2 кнопки. И там и там одинаковый функционал.


      1. TheShock
        14.01.2017 04:37

        Звучит как место для вывода кода в отдельный компонент. Но я не могу понять, что именно вы предлагаете.


      1. VolCh
        14.01.2017 07:31

        Как различать какие элементы демонтируются?


    1. faiwer
      14.01.2017 09:28
      +1

      не хватает аргументов, почему рекомендуют.

      У меня сложилось впечатление, что это для того, чтобы люди избегали использования ref как таковых, окромя тех ситуаций, когда иначе никак. Может быть с их точки зрения, люди злоупотребляют этой возможностью в ущерб react-way.


      1. VolCh
        14.01.2017 10:17
        +2

        Тогда надо сделать depricated, а потом убрать )


      1. mester
        14.01.2017 10:43
        +1

        Да, если добавить сюда ранее не документированный контекст, dangerouslySetInnerHTML={{ __html: «Hello» }}, вечные предупреждения при управлении div[contenteditable=«true»] — то получится сплошная политика запретов. VUE в этом плане куда свободнее


      1. TheShock
        14.01.2017 22:31

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

        И молодцы. Дали теперь возможность таким людям писать что-то вроде такого:

        <input ref={(input) => calculateSomething()} />
        


        Архитекторы очень мудры


    1. radist2s
      15.01.2017 00:34

      Вопрос к эксперту: а разве такого рода даже анонимные функции не успешно ли оптимизируются движком и фактически функция заново не создается? Или все же она сразу же выбрасывается из памяти после выполнения render?


      1. TheShock
        15.01.2017 01:19
        +2

        Если только с точки зрения теории, то стоит не забывать, что это не просто блок кода, а объект первого класса, он создается, к нему цепляется scope и prototype, его присваивают переменной. Чистые функции легче оптимизировать (там нет необходимости оставлять замыкание), эта функция грязная.

        На практике даже самые современные браузеры не всегда очищают память, если на неё была замкнута функция даже если эта функция уже неактуальна (это что-то вроде перекресных ссылок). И тем больше вероятность чем сложнее приложение, а одна неудачная неочищенная ссылка тянет за собой целый ворох мусора. А код реакта как раз крайне нетривиален.

        Более того, кроме чисто js-нюансов есть и важная особенность Реакта. Процитирую из документации:

        class LoggingButton extends React.Component {
          handleClick() {
            console.log('this is:', this);
          }
        
          render() {
            // This syntax ensures `this` is bound within handleClick
            return (
              <button onClick={(e) => this.handleClick(e)}>
                Click me
              </button>
            );
          }
        }
        


        The problem with this syntax is that a different callback is created each time the LoggingButton renders. In most cases, this is fine. However, if this callback is passed as a prop to lower components, those components might do an extra re-rendering. We generally recommend binding in the constructor or using the property initializer syntax, to avoid this sort of performance problem.


        То есть каждую перерисовку LoggingButton внутренний button будет получать НОВЫЙ экземпляр функции и потому Реакт будет считать, что его необходимо обновить, хотя можно было бы использовать закешированный (старый) вариант, если бы экземпляр функции с контекстом был создан и сохранен изначально.

        пс. Есть очень хорошая статья, которая поможет понять, что происходит и какая работа выполняется при создании анонимной функции: http://dmitrysoshnikov.com/ecmascript/ru-chapter-6-closures/


        1. SaitoNakamura
          17.01.2017 01:55
          +2

          То есть каждую перерисовку LoggingButton внутренний button будет получать НОВЫЙ экземпляр функции и потому Реакт будет считать, что его необходимо обновить, хотя можно было бы использовать закешированный (старый) вариант, если бы экземпляр функции с контекстом был создан и сохранен изначально.


          Следует понимать что это актуально только если реализовывается shouldComponentUpdate со сравнением функций переданных через props. Стандартная реализация shouldComponentUpdate просто возвращает true, поэтому компоненту глубоко плевать на то, новосозданная это функция или закешированная.

          П.С. вот хорошая статья на тему производительности реакта http://blog.csssr.ru/2016/12/07/react-perfomance/


          1. VolCh
            17.01.2017 18:32

            Или если что-то типа Redux/MobX/… подставляет свою функцию, о чём можно даже не догадываться при поверхностном изучении. Второй вариант — несколько этих компонентов на странице, в такой форме будет создаваться новая функция для каждого элемента, а могла бы одна биндиться.


    1. unel
      19.01.2017 19:47
      +1

      Вот тут участники реакта немного проясняют свою позицию:

      There are multiple problems with it:

      • It requires that React keeps track of currently rendering component (since it can't guess this). This makes React a bit slower.
      • It doesn't work as most people would expect with the «render callback» pattern (e.g.
        <DataGrid renderRow={this.renderRow} />
        
        ) because the ref would get placed on DataGrid for the above reason.
      • It is not composable, i.e. if a library puts a ref on the passed child, the user can't put another ref on it (e.g. #8734). Callback refs are perfectly composable.



      1. unel
        19.01.2017 19:52
        +1

        Кстати, эту информацию уже добавили в официальную доку, неплохо бы наверное и перевод обновить ;-)


        1. vtikunov
          19.01.2017 21:25

          Спасибо. Обновил.


  1. zShift
    14.01.2017 12:45
    -3

    Опять невалидное. Почему бы им просто не использовать data-атрибуты. Каждый пытается свою спецификацию сделать…


    1. faiwer
      14.01.2017 14:24

      Дык, эти атрибуты же не добираются до DOM. Это внутренняя кухня.


      1. zShift
        14.01.2017 14:42
        -2

        Если честно просто не пользуюсь реактом(и ангуляром из-за несоблюдения стандартов), но в любом случае этих атрибутом нет в спецификации. Есть data-атрибуты. Вы хотите сказать, что в выхлопе на HTML они урезаются чтоли или переформируются в валидный код?


        1. VolCh
          14.01.2017 15:51
          +4

          В Реакте не используется HTML, JSX (JS+XML) не надмножество ни HTML, ни даже xHTML. Как говорится, все элементы и атрибуты XML вымышлены, все совпадения с HTML случайны. JSX-элементы «рендерятся» в вызовы JS-функций CreateElement, которые возвращают элементы виртуального DOM, которые потом «рендерятся» в реальный. HTML в процессе вообще не участвует (серверный рендеринг опустим для простоты), а алгоритм маппинга виртуального DOM на реальный может быть произвольным. Стараются, конечно быть поближе к HTML, но священную корову из него не делают, не просто расширяя или урезая его, но и меняя синтаксис.

          В общем и в целом, часть атрибутов передаётся as is, часть переформатируется в валидный код, часть урезается.


  1. http3
    14.01.2017 21:06
    -9

    Кому надоел React, ставьте плюс. :)
    Кому нравиться, пройдите мимо. :)


    1. http3
      14.01.2017 22:17
      -6

      Просил же пройти мимо.
      Нет же, еще и в карму насрало.


      1. taujavarob
        16.01.2017 00:13
        +1

        На хабре полно людей, никогда не проходящих мимо.

        Особенно когда их об этом просишь. Специально не пройдут, ну и в карму специально не поленятся зайти.

        Терпите. :-)


    1. http3
      17.01.2017 11:35
      -2

      Астанавитесь. :)