React.js. Рефы и DOM-элементы

31.07.2021

Теги: DOMFrontendJavaScriptReact.jsWeb-разработкаКомпонент

В обычном потоке данных 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}
        />
    );
}
Вообще, эти два примера получились не слишком удачные — нарушается принцип инкапсуляции компонента. Но зато дает представление, как можно обходить обычный поток данных сверху вниз через пропсы. И как эти пропсы можно изменять, обеспечивая передачу данных снизу вверх без использования callback-функции (см. здесь).

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

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.