Рефы и 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
будет определён с привязанным к классу контекстом, но, заметим, что это не будет играть роли в большинстве случаев.