React.js. Рефы и DOM-элементы
31.07.2021
Теги: DOM • Frontend • JavaScript • React.js • Web-разработка • Компонент
В обычном потоке данных React родительские компоненты могут взаимодействовать с дочерними только через пропсы. Чтобы модифицировать потомка, мы должны заново отрендерить его с новыми пропсами. Тем не менее, могут возникать ситуации, когда требуется императивно изменить дочерний элемент, обойдя обычный поток данных. Подлежащий изменениям дочерний элемент может быть как React-компонентом, так и DOM-элементом. React предоставляет лазейку для обоих случаев.
Создание рефов
Рефы создаются с помощью React.createRef()
и прикрепляются к React-элементам через специальный ref
атрибут. Обычно рефы присваиваются свойству экземпляра класса в конструкторе, чтобы на них можно было ссылаться из любой части компонента.
class MyComponent extends React.Component { constructor(props) { super(props); this.divRef = React.createRef(); } render() { return <div ref={this.divRef} />; } }
Доступ к рефам
Когда реф передаётся элементу в методе render()
, ссылка на данный узел доступна через свойство рефа current
.
const divNode = this.divRef.current;
Значение рефа отличается в зависимости от типа узла:
- Когда атрибут
ref
используется с HTML-элементом, свойствоcurrent
объекта-рефа получает соответствующий DOM-элемент - Когда атрибут
ref
используется с классовым компонентом, свойствоcurrent
объекта-рефа получает экземпляр компонента - Нельзя использовать
ref
атрибут с функциональными компонентами, потому что для них не создаётся экземпляров
1. Получение ссылки на DOM-элемент
В представленном ниже примере мы сохраняем ссылку на DOM-элемент в свойстве mailInputRefer
экземпляра компонента Form
. Точнее говоря, ссылка на DOM-элемент будет в свойстве current
объекта mailInputRefer
.
class Form extends React.Component { constructor(props) { super(props); this.state = { mail: '' }; this.mailInputRefer = React.createRef(); } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * проверяем, что поле mail заполнено */ if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы this.mailInputRefer.current.style.backgroundColor = "#fdd"; // красный фон this.mailInputRefer.current.focus(); // устанавливаем фокус для ввода mail return; } else { this.mailInputRefer.current.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <input type="text" name="mail" value={this.state.mail} onChange={this.handleChange} ref={this.mailInputRefer} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
React присвоит DOM-элемент свойству current
при монтировании компонента и присвоит обратно значение null
при размонтировании. Обновление свойства происходит перед вызовом методов componentDidMount()
и componentDidUpdate()
.
2. Ссылка на классовый компонент
Все очень похоже на получение ссылки на DOM-элемент, только получаем ссылку на экземпляр класса компонента. В примере ниже в компоненте Form
мы сохраняем ссылку на комонент Input
. В самом комоненте Input
мы сохраняем ссылку на DOM-элемент input
. Тогда из компонента Form
мы можем через ссылку на компонент Input
получить доступ к элементу <input>
.
class Form extends React.Component { constructor(props) { super(props); this.state = { mail: '' }; // ссылка на экземпляр класса Input this.compInputRefer = React.createRef(); } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * Проверяем, что поле mail заполнено; из ссылки на компонент Input достаем * ссылку на текстовое поле input — и дальше работаем с этим DOM-элементом */ const mailInputRefer = this.compInputRefer.current.textInputRefer.current; if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы mailInputRefer.style.backgroundColor = "#fdd"; // красный фон mailInputRefer.focus(); // устанавливаем фокус для ввода mail return; } else { mailInputRefer.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <Input name="mail" value={this.state.mail} changeHandler={this.handleChange} ref={this.compInputRefer} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
class Input extends React.Component { constructor(props) { super(props); // ссылка на DOM-элемент <input/> this.textInputRefer = React.createRef(); } render() { return ( <input type="text" name={this.props.name} value={this.props.value} onChange={this.props.changeHandler} ref={this.textInputRefer} placeholder={this.props.placeholder} /> ); } }
Пожалуй, здесь получилось слишком сложно — ссылка на компонент, ссылка на DOM-элемент, через одну ссылку дотягиваемся до второй. Можно сделать проще — компонент Form
передает через пропсы компоненту Input
тот объект, который создает createRef()
, а компонент Input
передает тот же самый объект элементу <input>
через специальный атрибут ref
.
class Form extends React.Component { constructor(props) { super(props); this.state = { mail: '' }; // здесь будет ссылка на DOM-элемент input this.mailInputRefer = React.createRef(); } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * проверяем, что поле mail заполнено */ if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы this.mailInputRefer.current.style.backgroundColor = "#fdd"; // красный фон this.mailInputRefer.current.focus(); // устанавливаем фокус для ввода mail return; } else { this.mailInputRefer.current.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <Input name="mail" value={this.state.mail} changeHandler={this.handleChange} reference={this.mailInputRefer} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
function Input(props) { return ( <input type="text" name={props.name} value={props.value} onChange={props.changeHandler} ref={props.reference} placeholder={props.placeholder} /> ); }
3. Ссылка на функциональный компонент
Нельзя получить ссылку на функциональный компонент, так как у него нет экземпляра. Представленный ниже код не будет работать:
class Form extends React.Component { constructor(props) { super(props); this.state = { mail: '' }; this.textInputRefer = React.createRef(); } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { event.preventDefault(); console.log(this.textInputRefer.current); // null, нельзя получить ссылку } render() { return ( <form onSubmit={this.handleSubmit}> <Input name="mail" value={this.state.mail} changeHandler={this.handleChange} ref={this.textInputRefer} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
function Input(props) { return ( <input type="text" name={props.name} value={props.value} onChange={props.changeHandler} placeholder={props.placeholder} /> ); }
Нужно преобразовать компонент в класс, чтобы можно было получить ссылку на него. Точно так же мы делаем, когда необходимо наделить компонент методами жизненного цикла и состоянием. Тем не менее, можно использовать атрибут ref
внутри функционального компонента, если получать ссылку на DOM-элемент или экземпляр класса компонента.
// получаем ссылку на DOM-элемент input function CustomInput(props) { let textInputRefer = React.createRef(); function handleClick() { textInputRefer.current.focus(); } return ( <div> <input type="text" name={props.name} value={props.value} ref={textInputRefer} /> <button onClick={handleClick}>Установить фокус</button> </div> ); }
Получение ссылки через callback-функцию
Кроме того, React поддерживает другой способ определения рефов, который называется «колбэк-рефы». Вместо того, чтобы передавать в специальный атрибут ref
объект, созданный с помощью createRef()
, мы можем передать в него функцию. Данная функция получит React-компонент или DOM-элемент в качестве аргумента, который мы можем сохранить, чтобы использовать в любом другом месте.
В примере ниже специальному атрибуту ref
передается колбэк-функция, которая сохраняет ссылку на DOM-элемент в свойстве mailInputRefer
экземпляра компонента Form
.
class Form extends React.Component { state = { mail: '' } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * проверяем, что поле mail заполнено */ if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы this.mailInputRefer.style.backgroundColor = "#fdd"; // красный фон this.mailInputRefer.focus(); // устанавливаем фокус для ввода mail return; } else { this.mailInputRefer.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <input type="text" name="mail" value={this.state.mail} onChange={this.handleChange} ref={element => this.mailInputRefer = element} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
React вызовет реф-колбэк с DOM-элементом <input>
при монтировании компонента, а также вызовет его со значением null
при размонтировании. Рефы будут хранить актуальное значение перед вызовом методов componentDidMount()
или componentDidUpdate()
.
Мы можем передавать колбэк-рефы между компонентами точно так же, как и объектные рефы, созданные через React.createRef()
.
class Form extends React.Component { state = { mail: '' } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * проверяем, что поле mail заполнено */ if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы this.mailInputRefer.style.backgroundColor = "#fdd"; // красный фон this.mailInputRefer.focus(); // устанавливаем фокус для ввода mail return; } else { this.mailInputRefer.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <Input name="mail" value={this.state.mail} changeHandler={this.handleChange} reference={element => this.mailInputRefer = element} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
function Input(props) { return ( <input type="text" name={props.name} value={props.value} onChange={props.changeHandler} ref={props.reference} placeholder={props.placeholder} /> ); }
Компонент Form
передаёт свой колбэк-реф как проп reference
компоненту Input
, а компонент Input
передаёт ту же самую функцию через специальный атрибут ref
элементу <input>
. В итоге свойство emailInputRefer
компонента Form
будет хранить значение DOM-узла, соответствующего элементу <input>
в компоненте Input
.
Перенаправление рефов
Собственно, мы уже делали нечто подобное, когда в компоненте Form
получали ссылку на дочерний компонент Input
, а через ссылку на Input
добирались до DOM-элемента <input>
. Но React предлагает свой вариант, который явно указывает на то, что нужно перенаправить реф и получить ссылку на DOM-элемент в дочернем компоненте.
class Form extends React.Component { constructor(props) { super(props); this.state = { mail: '' }; this.mailInputRefer = React.createRef(); } handleChange = (event) => { this.setState({[event.target.name]: event.target.value}); } handleSubmit = (event) => { /* * проверяем, что поле mail заполнено */ if (this.state.mail.trim() === "") { event.preventDefault(); // отменяем отправку формы this.mailInputRefer.current.style.backgroundColor = "#fdd"; // красный фон this.mailInputRefer.current.focus(); // устанавливаем фокус для ввода mail return; } else { this.mailInputRefer.current.style.backgroundColor = ""; } } render() { return ( <form onSubmit={this.handleSubmit}> <Input name="mail" value={this.state.mail} changeHandler={this.handleChange} ref={this.mailInputRefer} placeholder="Адрес почты" /> <input type="submit" value="Отправить" /> </form> ); } }
const Input = React.forwardRef((props, refer) => ( <input type="text" name={props.name} value={props.value} onChange={props.changeHandler} ref={refer} placeholder={props.placeholder} /> ));
Второй аргумент refer
существует только в том случае, если мы создаем компонент через функцию React.forwardRef()
. Обычные функциональные или классовые компоненты не получают refer
в качестве аргумента или пропа.
Поиск: DOM • JavaScript • React.js • Web-разработка • Frontend • Компонент • forwardRef • Реф