Всё под контролем: сила useRef и forwardRef в React
- вторник, 19 ноября 2024 г. в 00:00:04
Начнем с небольшой истории. Как-то раз я с размахом накинулся на проект — в духе нынешних фреймворков всё было обложено компонентами, декларативный стиль царил, все шло идеально… ну почти. Дошел я, значит, до нужды контролировать DOM-узлы напрямую. И что вы думаете? Прямого доступа нет, React закрыл от меня этот мир, сидит и ухмыляется: мол, мы тут за производительность боремся, зачем тебе что-то трогать руками?
Но мы не из тех, кто сдаётся, верно? React предлагает своё решение — рефы, и именно о них сегодня пойдет речь.
В React, благодаря его декларативной природе, мы описываем, что мы хотим от интерфейса, а как это будет достигнуто, фреймворк решает за нас. Но бывают ситуации, когда нужно среагировать напрямую на изменения в DOM, например:
Внедрить фокус на элемент при загрузке страницы или при каком-то событии.
Управлять анимацией, отслеживать её этапы или прерывать по нужде.
Получить размеры или позицию DOM-узла для расчётов.
Рефы и были созданы для таких ситуаций. Они позволяют подключиться к нужным узлам, сохраняя при этом общий подход React к управлению компонентами.
Хук useRef
— это инструмент, который позволяет сохранять ссылки на DOM-элементы или любые другие значения, которые нужно сохранить между рендерами. В отличие от useState
, изменение значения, хранимого в useRef
, не вызывает повторного рендера компонента.
Когда мы вызываем useRef
(например, const inputRef = useRef(null);
), React создает объект с единственным свойством current
, которое мы можем использовать для хранения значения, как в обычном контейнере. Это свойство current
инициализируется значением, переданным в useRef
, и остается доступным даже после повторного рендера. Отмечаю, что каждый раз, когда мы обновляем inputRef.current
, React не реагирует на это обновление рендерингом.
Рассмотрим пример кода, где useRef
используется для установки фокуса на текстовое поле при монтировании компонента.
import React, { useRef, useEffect } from 'react';
const TextInput = () =>; {
const inputRef = useRef(null); // создаем реф
useEffect(() =>; {
inputRef.current.focus(); // фокусируем элемент при монтировании
}, []);
return <input placeholder="Введи что-нибудь..." type="text">;
};
Вот что здесь происходит:
const inputRef = useRef(null);
создает реф-контейнер, который сохраняется между рендерами. В JSX мы передаем inputRef
в атрибут ref
у <input>
, и React автоматически связывает inputRef.current
с этим DOM-элементом.
Когда компонент монтируется, useEffect
вызывает inputRef.current.focus()
, что устанавливает фокус на поле ввода сразу при загрузке, без лишнего состояния и дополнительных манипуляций.
forwardRef
— это специальная функция React, которая позволяет передавать рефы через компоненты в глубину дерева компонентов. По дефолту если мы передаем реф в кастомный компонент, он воспринимается как обычный пропс и не получает спец. статуса, как это было бы при прямой передаче в DOM-элемент. forwardRef
решает эту проблему, делая реф доступным на любом уровне вложенности.
Функция forwardRef
позволяет «обернуть» компонент, чтобы он мог принимать и обрабатывать реф, передаваемый родительским компонентом.
Рассмотрим пример, в котором есть кастомная кнопка FancyButton
, и родительский компонент хочет управлять ее фокусом.
import React, { forwardRef, useRef } from 'react';
// Компонент FancyButton использует forwardRef для получения доступа к ref
const FancyButton = forwardRef((props, ref) =>; (
<button>
{props.children}
</button>
));
const ParentComponent = () =>; {
const buttonRef = useRef(null); // создаем реф для передачи в дочерний компонент
const handleClick = () =>; {
// Используем реф для доступа к DOM-узлу кнопки
if (buttonRef.current) {
buttonRef.current.focus(); // Устанавливаем фокус на кнопке
console.log("Фокус на кнопке установлен!");
}
};
return (
<div>
Нажми меня
<button>Фокус на первой кнопке</button>
</div>
);
};
export default ParentComponent;
Поясню код:
В FancyButton
используем forwardRef
, чтобы реф, переданный родителем, мог быть связан с внутренним <button>
. Это позволяет FancyButton
принимать ref
и делиться доступом к DOM-узлу на уровне родителя.
В ParentComponent
создаем buttonRef
через useRef
, передаем его в FancyButton
, и теперь buttonRef.current
даёт доступ к кнопке внутри. В handleClick
проверяем buttonRef
, и, если он ссылается на DOM-узел, устанавливаем на кнопку фокус. Без forwardRef
React воспринимал бы ref
как обычный пропс, и мы не могли бы управлять DOM напрямую.
Рассмотрим комплексный пример, в которомзадействуем и useRef
, и forwardRef
. Создадим компонент с полем ввода, и обернем его во внешний компонент с кнопками для управления фокусом и очистки значения поля.
Будет три компонента:
TextInput
— это компонент, представляющий текстовое поле. Он принимает реф, который позволяет внешнему компоненту управлять его состоянием.
TextInputWrapper
— обертка вокруг TextInput
, использующая forwardRef
для проброса рефа.
ParentComponent
— родительский компонент, который создает реф и управляет взаимодействием с TextInputWrapper
через кнопки.
Код:
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
// Компонент TextInput с управлением через ref
const TextInput = forwardRef((props, ref) =>; {
const inputRef = useRef(null);
// Экспортируем методы, которые будут доступны родительским компонентам
useImperativeHandle(ref, () =>; ({
focus: () =>; {
inputRef.current.focus();
},
clear: () =>; {
inputRef.current.value = '';
}
}));
return <input style="{{" placeholder="Введите текст..." type="text">;
});
// Компонент-обертка TextInputWrapper, который пробрасывает реф вниз
const TextInputWrapper = forwardRef((props, ref) =>; {
return (
<div style="{{">
</div>
);
});
// Родительский компонент, управляющий состоянием TextInput
const ParentComponent = () =>; {
const inputRef = useRef(null);
const handleFocus = () =>; {
if (inputRef.current) {
inputRef.current.focus();
}
};
const handleClear = () =>; {
if (inputRef.current) {
inputRef.current.clear();
}
};
return (
<div>
<h1>Демонстрация использования useRef и forwardRef</h1>
<button style="{{">
Фокус на поле
</button>
<button style="{{">
Очистить поле
</button>
</div>
);
};
export default ParentComponent;
TextInput — это простое поле ввода, в котором мы через useRef
создаем реф inputRef
для доступа к самому <input>
. С помощью useImperativeHandle
добавляем два метода — focus
и clear
, чтобы родительский компонент мог фокусироваться на поле или очищать его, не заглядывая внутрь. Пробрасываем этот реф с помощью forwardRef
, чтобы дать родителю полный контроль.
TextInputWrapper и ParentComponent: TextInputWrapper
— это обертка для TextInput
, показывающая, как пробросить реф еще дальше. В ParentComponent
мы создаем реф inputRef
и передаем его через обертку. Кнопки "Фокус" и "Очистить" используют этот реф, чтобы управлять TextInput
, не вмешиваясь в его внутренности — все происходит через один общий реф, гибко и чисто.
Запомните: рефы — это оружие с тонким лезвием. Пользуйтесь ими аккуратно, с умом, и только когда это действительно оправдано.
Всем разработчикам, использующим React, рекомендую к посещению открытый урок 26 ноября — урок пройдет на тему применения хуков и мемоизации для оптимизации React-приложений. Записаться можно на странице курса "React.js Developer".