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 • Реф