Рефи та DOM

Рефи надають доступ до DOM-вузлів чи React-елементів, що створюються під час рендеру.

У звичайному потоці даних React, батьківські компоненти можуть взаємодіяти з дітьми тільки через пропси. Щоб модифікувати нащадка, ви маєте повторно відрендерити його з новими пропсами. Проте існують ситуації, коли вам потрібно імперативно змінити нащадка поза межами звичайного потоку даних. Нащадок, щоб бути зміненим, має бути екземпляром React-компонента або DOM-елементом. В обох випадках React надає можливість обійти звичайний потік даних.

Коли використовувати рефи

Існує декілька ситуацій, коли доцільно використовувати рефи:

  • Контроль фокусу, виділення тексту чи контроль програвання медіа.
  • Виклик імперативної анімації.
  • Інтеграція зі сторонніми DOM-бібліотеками.

Уникайте використання рефів для будь-чого, що можна зробити декларативно.

Наприклад, замість виклику методів open() та close() компоненту Dialog, передайте йому проп isOpen.

Не зловживайте рефами

Мабуть першим вашим бажанням буде використовувати рефи для того, щоб “все працювало” у вашому додатку. Якщо це так, зупиніться та подумайте більш критично про те, який з компонентів в ієрархії має контролювати стан. Зазвичай, стає зрозуміло, що правильне місце для контролю стану лежить на верхніх рівнях ієрархії. Більш детально ознайомтеся з розділом Підйом стану для прикладів такої поведінки.

Примітка:

Приклади нижче були оновлені для використання React.createRef() API, що з’явився у 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-елемента, тоді ref, що створений у конструкторі за допомогою методу React.createRef(), отримує доступ до відповідого DOM-елемента через свою властивість current.
  • Коли атрибут ref визначений у компонента користувача, тоді об’єкт ref у свою властивість current отримує посилання на примонтований екземпляр компонента.
  • Заборонено використовувати атрибут ref з функціональними компонентами, тому що у них немає екземплярів.

На прикладах нижче можна побачити різницю.

Застосування рефу до DOM-елемента

Код нижче використовує ref, щоб отримати посилання на DOM-вузол:

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

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

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

React зв’яже властивість current з DOM-елементом, коли компонент буде примонтований, та встановить назад у null, коли компонент буде прибрано з DOM. Оновлення ref відбувається перед componentDidMount або componentDidUpdate.

Застосування рефу до компонента

Якби ми захотіли обернути попередній компонент CustomTextInput, щоб симулювати натискання по ньому відразу після монтування, ми могли б використати реф, щоб отримати доступ до користувацького поля введення та викликати його метод 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} />
    );
  }
}

Ви маєте перетворити компонент у клас, якщо ви хочете скористатися рефом, так само як ви робили б, коли вам потрібні методи життєвого циклу або стан.

Проте ви можете використовувати атрибут ref в середині функціональних компонентів за умови, що ви визначаєте їх на DOM-елементах або класових компонентах:

function CustomTextInput(props) {
  // textInput повинен бути визначений тут, щоб реф міг посилатися на нього
  let textInput = React.createRef();

  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 або нижче, або ви потребуєте більшої гнучкості, ніж вам дає перенаправлення рефів, ви можете скористатися альтернативним підходом та явно передати реф як проп з іменем відмінним від ref.

Ми не рекомендуємо підхід передачі DOM-вузлів, але він може стати рятувальним жилетом. Зверніть увагу, що цей підхід потребує написання додаткового коду для дочірніх комопонетів. Якщо у вас взагалі немає котролю над реалізацією дочірніх компонентів, то ваш остання можливість — скористатися методом findDOMNode(), але цей метод нерекомендований та вважається застарілим у StrictMode.

Рефи зворотнього виклику

React також підтримує інший варіант ініціалізації рефів, що називається “рефи зворотнього виклику” (“callback refs”), що дає більший контроль над процесом визначення та очищення рефів.

На відміну від передачі рефа, що створений функцією createRef(), через атрибути ref, ви передаєте функцію. Функція отримує екземпляр компонента чи 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() {
    // автоматичний фокус на полі введення при монтуванні
    this.focusTextInput();
  }

  render() {
    // Використання функції зворотнього виклику в `ref` для зберігання посилання на DOM-елемент
    // текстове поле введення в екземплярі (наприклад, this.textInput).
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Фокус на текстовому полі введення"
          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. Ми не радимо користуватися ним, тому що рядкові рефи мають деякі проблеми, також цей API вважається застарілим, та ймовірно буде видалений в одній з майбутніх версій.

Примітка:

Якщо ви досі користуєтесь this.refs.textInput для доступу до рефів, ми рекомендуємо натомість користовуватися рефами зворотнього виклику або createRef API.

Застереження до рефів зворотнього виклику

Якщо ref визначено як вбудовану функцію, то вона буде виклакана двічі протягом оновлень, перший раз з null, потім з посиланням на DOM-елемент. Це відбувається, тому що створюється новий екземпляр функції під час кожного рендеру, так як React потребує очистити старий реф та встановити новий. Щоб запобігти цьому, просто передайте в ref метод класу, але зверніть увагу, що в більшості випадків це не має значення.