Рефы и DOM

Рефы дают возможность получить доступ к DOM-узлам или React-элементам, созданным в рендер-методе.

В обычном потоке данных React родительские компоненты могут взаимодействовать с дочерними только через пропсы. Чтобы модифицировать потомка, вы должны заново отрендерить его с новыми пропсами. Тем не менее, могут возникать ситуации, когда вам требуется императивно изменить дочерний элемент, обойдя обычный поток данных. Подлежащий изменениям дочерний элемент может быть как React-компонентом, так и DOM-элементом. React предоставляет лазейку для обоих случаев.

Когда использовать рефы

Ситуации, в которых использование рефов является оправданным:

  • Управление фокусом, выделение текста или воспроизведение медиа.
  • Императивный вызов анимаций.
  • Интеграция со сторонними DOM-библиотеками.

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

Например, вместо того чтобы определять методы open() и close() в компоненте Dialog, лучше передавать ему проп isOpen.

Не злоупотребляйте рефами

Возможно, с первого взгляда вам показалось, что рефы применяются, когда нужно решить какую-то задачу в вашем приложении «во что бы то ни стало». Если у вас сложилось такое впечатление, сделайте паузу и обдумайте, где должно храниться конкретное состояние в иерархии компонентов. Часто становится очевидно, что правильным местом для хранения состояния является верхний уровень в иерархии. Подробнее об этом — в главе Подъём состояния.

Примечание

Приведённые ниже примеры были обновлены с использованием API-метода React.createRef() добавленного в React 16.3. Если вы используете более старую версию React, мы рекомендуем использовать колбэк-рефы.

Создание рефов

Рефы создаются с помощью React.createRef() и прикрепляются к React-элементам через ref атрибут. Обычно рефы присваиваются свойству экземпляра класса в конструкторе, чтобы на них можно было ссылаться из любой части компонента.

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();  }
  render() {
    return <div ref={this.myRef} />;  }
}

Доступ к рефам

Когда реф передаётся элементу в методе render, ссылка на данный узел доступна через свойство рефа current.

const node = this.myRef.current;

Значение рефа отличается в зависимости от типа узла:

  • Когда атрибут ref используется с HTML-элементом, свойство current созданного рефа в конструкторе с помощью React.createRef() получает соответствующий DOM-элемент.
  • Когда атрибут ref используется с классовым компонентом, свойство current объекта-рефа получает экземпляр смонтированного компонента.
  • Нельзя использовать ref атрибут с функциональными компонентами, потому что для них не создаётся экземпляров.

Представленные ниже примеры демонстрируют отличия в зависимости от типа узла.

Добавление рефа к DOM-элементу

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

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // создадим реф в поле `textInput` для хранения DOM-элемента
    this.textInput = React.createRef();    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // Установим фокус на текстовое поле с помощью чистого DOM API
    // Примечание: обращаемся к "current", чтобы получить DOM-узел
    this.textInput.current.focus();  }

  render() {
    // описываем, что мы хотим связать реф <input>
    // с `textInput` созданным в конструкторе
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />        <input
          type="button"
          value="Фокус на текстовом поле"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

React присвоит DOM-элемент свойству current при монтировании компонента и присвоит обратно значение null при размонтировании. Обновление свойства ref происходит перед вызовом методов componentDidMount и componentDidUpdate.

Добавление рефа к классовому компоненту

Для того чтобы произвести имитацию клика по CustomTextInput из прошлого примера сразу же после монтирования, можно использовать реф, чтобы получить доступ к пользовательскому <input> и явно вызвать его метод focusTextInput:

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();  }

  componentDidMount() {
    this.textInput.current.focusTextInput();  }

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

Обратите внимание, что это сработает только в том случае, если CustomTextInput объявлен как классовый компонент:

class CustomTextInput extends React.Component {  // ...
}

Рефы и функциональные компоненты

По умолчанию нельзя использовать атрибут ref с функциональными компонентами, потому что для них не создаётся экземпляров:

function MyFunctionComponent() {  return <input />;
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();  }
  render() {
    // Данный код *не будет* работать!
    return (
      <MyFunctionComponent ref={this.textInput} />    );
  }
}

Если вам нужен реф на функциональный компонент, можете воспользоваться forwardRef (возможно вместе с useImperativeHandle), либо превратить его в классовый компонент.

Тем не менее, можно использовать атрибут ref внутри функционального компонента при условии, что он ссылается на DOM-элемент или классовый компонент:

function CustomTextInput(props) {
  // textInput должна быть объявлена здесь, чтобы реф мог иметь к ней доступ  const textInput = useRef(null);
  function handleClick() {
    textInput.current.focus();  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} />      <input
        type="button"
        value="Фокус на поле для ввода текста"
        onClick={handleClick}
      />
    </div>
  );
}

Передача DOM-рефов родительским компонентам

В редких случаях вам может понадобиться доступ к дочернему DOM-узлу из родительского компонента. В общем случае, такой подход не рекомендуется, т. к. ведёт к нарушению инкапсуляции компонента, но иногда он может пригодиться для задания фокуса или измерения размеров, или положения дочернего DOM-узла.

Несмотря на то, что можно было бы добавить реф к дочернему компоненту, такое решение не является идеальным, т. к. вы получите экземпляр компонента вместо DOM-узла. Кроме того, это не сработает с функциональными компонентами.

Если вы работаете с React 16.3 или новее, мы рекомендуем использовать перенаправление рефов для таких случаев. Перенаправление рефов позволяет компонентам осуществлять передачу рефа любого дочернего компонента как своего собственного. Вы можете найти детальные примеры того, как передать дочерний DOM-узел родительскому компоненту в документации по перенаправлению рефов.

Если вы используете React версии 16.2 или ниже, или если вам нужно решение более гибкое, чем перенаправление рефов, вы можете использовать данный альтернативный подход и явно передавать реф как проп с другим именем.

По возможности, мы советуем избегать передачи DOM-узлов, но это может быть полезной лазейкой. Заметим, что данный подход требует добавления кода в дочерний компонент. Если у вас нет никакого контроля над реализацией дочернего компонента, последним вариантом является использование findDOMNode(), но такое решение не рекомендуется и не поддерживается в StrictMode.

Колбэк-рефы

Кроме того, React поддерживает другой способ определения рефов, который называется «колбэк-рефы» и предоставляет более полный контроль над их присвоением и сбросом.

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

Представленный ниже пример реализует общий паттерн: использование колбэка в ref для хранения ссылки на DOM-узел в свойстве экземпляра.

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;
    this.setTextInputRef = element => {      this.textInput = element;    };
    this.focusTextInput = () => {      // Устанавливаем фокус на текстовом поле ввода с помощью чистого DOM API      if (this.textInput) this.textInput.focus();    };  }

  componentDidMount() {
    // устанавливаем фокус на input при монтировании
    this.focusTextInput();  }

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

React вызовет ref колбэк с DOM-элементом при монтировании компонента, а также вызовет его со значением null при размонтировании. Рефы будут хранить актуальное значение перед вызовом методов componentDidMount или componentDidUpdate.

Вы можете передавать колбэк-рефы между компонентами точно так же, как и объектные рефы, созданные через React.createRef().

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}      />
    );
  }
}

В представленном выше примере, Parent передаёт свой колбэк-реф как проп inputRef компоненту CustomTextInput, а CustomTextInput передаёт ту же самую функцию как специальный атрибут ref элементу <input>. В итоге свойство this.inputElement компонента Parent будет хранить значение DOM-узла, соответствующего элементу <input> в CustomTextInput.

Устаревший API: строковые рефы

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

Примечание

Если вы используете this.refs.textInput для доступа к рефам в своих проектах, мы рекомендуем перейти к использованию паттерна с колбэком или createRef API.

Предостережения насчёт колбэк-рефов

Если ref колбэк определён как встроенная функция, колбэк будет вызван дважды во время обновлений: первый раз со значением null, а затем снова с DOM-элементом. Это связано с тем, что с каждым рендером создаётся новый экземпляр функции, поэтому React должен очистить старый реф и задать новый. Такого поведения можно избежать, если колбэк в ref будет определён с привязанным к классу контекстом, но, заметим, что это не будет играть роли в большинстве случаев.