
Использование ref в функциональных компонентах играет две роли:
-
С помощью них можно получить ссылку на dom элемент
-
ref можно использовать как стабильные переменные.
В этой статье сосредоточимся на первой роли, разберем, как с помощью ref получить доступ к dom элементам и компонентам react, включая такие какие способы как createRef, useRef и ref callback. Обсудим для чего нужны forwardRef и useImperativeHandle , и как с их помощью получить ссылку на функциональные компоненты, спойлер: нельзя так просто получить ссылку на функциональный компонент с помощью ref. А уже в следующей статье обсудим роль ref в качестве стабильной переменной, и как это облегчит нам жизнь при использовании useEffect, useMemo, useCallback.
Что такое ref и зачем они нужны?
ref или по-другому reference — это ссылка и как можно понять из названия, в контексте react это ссылка на элемент.
Допустим есть div с текстом и нужно получить ссылку на него, чтобы узнать его ширину, как мы можем это сделать? В классическом javascript мы можем использовать document.getElementById('id') или document.querySelector('div').
Классический подход
// html <div id="custom-id">Элемент с текстом</div> <script> const div = document.getElementById('id'); console.log(div.offsetWidth); // получаем ширину элемента </script>
В классическом подходе мы, скорее всего, не будем добавлять/удалять элементы dom дерева, в react же постоянно это происходит и если мы попробуем использовать getElementById/querySelector можем столкнуться с ситуацией, когда элемента просто нет в dom дереве. Чтобы это работало придется написать дополнительный код, используя useEffect в функциональных компонентах или метод componentDidMount в классовых компонентах.
Чтобы получить ссылку на dom элемент, можно попробовать использовать document.getElementById('id') в методе render, но это не сработает, потому что целевой div еще не отрендерен. На момент выполнения getElementById его еще нет в dom.
Разработка на react. Не рабочий вариант
class ClassComponent extends Component { render() { const div = document.getElementById('id'); // получим undefined, т.к. целевой div еще не отрендерен console.log(div?.offsetWidth); return <div id="id">Элемент с текстом</div> } } const FuncComponent = () => { const div = document.getElementById('id'); // получим undefined, т.к. целевой div еще не отрендерен console.log(div?.offsetWidth); return <div id="id">Элемент с текстом</div> }
Чтобы получить ссылку на элемент, нужно использовать document.getElementById('id') в момент после рендеринга. В классовом компоненте это componentDidMount, в функциональном компоненте useEffect.
Разработка на react. Рабочий вариант без ref
class ClassComponent extends Component { componentDidMount() { const div = document.getElementById('id'); console.log(div.offsetWidth); // получаем ширину элемента } render() { return <div id="id">Элемент с текстом</div> } } const FuncComponent = () => { useEffect(() => { const div = document.getElementById('id'); console.log(div.offsetWidth); // получаем ширину элемента }, []); return <div id="id">Элемент с текстом</div> }
Да, это рабочий вариант, но рекомендуется использовать ref api.
Как получить ссылку на dom элемент
В react у каждого элемента есть атрибут ref. Ниже приведен рабочий код с использованием ref. Обратите внимание на переменную element. В обоих случаях это объект со свойством current и это просто соглашение. Также обратите внимание, что по-прежнему получить свойства элемента можно только в componentDidMount/useEffect. Важно понимать, что получить ссылку на любой элемент можно только после рендеринга этого элемента, именно поэтому нужно использовать componentDidMount/useEffect
Разработка на react. Рабочий вариант с использованием ref api
class ClassComponent extends Component { element = { current: null }; componentDidMount() { // после рендеринга в this.element.current попадет ссылка на элемент console.log(this.element.current.offsetWidth); } render() { return <div ref={this.element}>Элемент с текстом</div> } } const FuncComponent = () => { const element = useRef<HTMLDivElement>(); useEffect(() => { // после рендеринга в element.current попадет ссылка на элемент console.log(element.current.offsetWidth); }, []); return <div ref={element}>Элемент с текстом</div> }
Типизация useRef выглядит вот так
// Вспомогальный тип interface MutableRefObject<T> { current: T; } function useRef<T>(initialValue?: T): MutableRefObject<T>
Обращаю внимание, это дженерик, то есть можно передать тип данных, которые будут храниться в useRef. А также этот хук имеет один необязательный аргумент — начальное значение. Этот аргумент необходим в контексте сохранения стабильной переменной.
Функция createRef
Есть специальная функция createRef, она возвращает { current: null } и по сути является синтаксическим сахаром.
class ClassComponent extends Component { // эти две записи аналогичны element = { current: null }; element = createRef(); componentDidMount() { // после рендеринга в this.element.current попадет ссылка на элемент console.log(this.element.current.offsetWidth); } render() { return <div ref={this.element}>Элемент с текстом</div> } }
Типизация createRef
interface RefObject<T> { readonly current: T | null; } function createRef<T>(): RefObject<T>;
Заметили? useRef возвращает MutableRefObject, то есть объект вида { current: T }, который можно изменять, а вот createRef возвращает объект { readonly current: T }, соответственно не предполагается изменение значения current.
Как получить ссылку на html элемент и классовый компонент
У каждого react компонента также есть атрибут refи с его помощью можно получить внутренние методы и переменные классовых компонентов.

В консоли мы на строке 15 получим вот такие данные

То есть получим доступ к внутренним данным компонента: не только к его переменным и методам, что мы указали, но также и к props, setState, forceUpdate и прочему.
Пример кода тут.
Также в атрибут ref можно передавать не только объект вида { current: T }, но и функцию, которая единственным аргументом принимает ссылку на элемент, этот прием называется ref callback:
class ClassComponent extends Component { element = null; componentDidMount() { // после рендеринга в this.element попадет ссылка на элемент console.log(this.element.offsetWidth); } render() { return <div ref={elem => (this.element = elem)}>Элемент с текстом</div> } }
Однако, с функциональными компонентами мы получим другой результат.

Уже при попытке указать ref для функционального компонента получим ошибку typescript, а в консоли увидим следующее предупреждение и получим undefined вместо данных компонента.

Как получить ссылку на html элемент внутри функционального компонента. Использование forwardRef
Как следует из названия forwardRef — «перенаправить ref», это и есть суть этой функции. forwardRef — это функция, которая принимает функциональный компонент и возвращает новый функциональный компонент, который может иметь атрибут ref.
Вот так выглядит типизация
interface ForwardRefRenderFunction<T, P = {}> { (props: PropsWithChildren<P>, ref: ForwardedRef<T>): ReactElement | null; displayName?: string; // explicit rejected with `never` required due to // https://github.com/microsoft/TypeScript/issues/36826 /** * defaultProps are not supported on render functions */ defaultProps?: never; /** * propTypes are not supported on render functions */ propTypes?: never; } interface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> { defaultProps?: Partial<P>; propTypes?: WeakValidationMap<P>; } function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
Она может показаться страшной, на практике же, изменяется всего две вещи:
-
forwardRefэто дженерик и ему нужно передать 2 типа: типrefи типprops, подробнее ниже. -
У функционального компонента появляется второй аргумент —
ref.
В общем виде компонент с использованием forwardRef выглядит так.
const FuncComponent = forwardRef<RefType, PropsType>((props: PropsType, ref: RefType) => { ... })
Посмотрите на этот код

Здесь реализовано перенаправление ссылки на div элемент. Посмотрите на forwardRef<HTMLDivElement>, первым типом мы передали HTMLDivElement, это значит, что ref в нашем случае будет именно div-ом. И обратите внимание, наш TestFuncBase компонент вторым аргументом, получает ref аргумент (3 строка) и мы передаем его в ref атрибут (4 строка). Поэкспериментировать с примером можно тут.
Как получить ссылку на функциональный компонент. forwardRef + useImperativeHandle
Мы узнали, как перенаправить на html элемент ref, который используем для функционального компонента, но это все еще не дотягивает до случая с классовым компонентом, когда мы может из компонента подтянуть состояние, изменить его состояние, получить внутренние переменные и методы. Можно ли воссоздать эти возможности при работе с функциональными компонентами? Да, можно, но не в полной мере или скорее по-другому.
Существует специальный хук useImperativeHandle, миссия которого записывать в ref любые данные из функционального компонента.
Его типизация выглядит так:
function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps?: any[]): void;
Это дженерик, первым аргументом принимает тип ref, который должен полностью совпадать в типом ref, что прилетел вторым аргументом функционального компонента. Второй тип — полностью с типом ref, либо может содержать дополнительные свойства.
Если говорить об этом хуке, как о функции, то первым аргументом он принимает ref, который прилетел в функциональный компонент извне. Вторым аргументом принимает функцию (init), которая возвращает объект, часть ref. Эта часть будет добавлена в ref. А третий аргумент (deps) — это массив зависимостей.
Работа в связке с forwardRef может выглядеть так:

Поиграть с примером можно тут. Обратите внимание, не использую массив зависимостей, об этом ниже.
Функция init, как и в остальных хуках, клонируется и в ней присутствует проблема потери замыкания, о которой мы говорили в прошлой статье. И у нас есть два пути. Первый — можно не указывать массив зависимостей, т.к. это опциональный аргумент, благодаря этому будет клонироваться каждый раз новая функция со всеми замыканиями и ошибок не будет. Второй — можно указать в массиве зависимостей все переменные, которые могут потеряться, впрочем eslint плагин для хуков подскажет, что нужно добавить.
Я предпочитаю первый способ, потому как ссылка на функциональный компонент создается с помощью useRef, а значит всегда попадает в замыкания любых сторонних хуков и нет надобности сохранять в ref одни и те же ссылки методов. Единственный случай, на мой взгляд, когда нужен массив зависимостей, когда из функционального компонента получаем методы (ссылочный тип) и эти методы передаем сразу в другой мемоизированный компонент.
type FuncForwardRefComponentRef = { onClick: () => void; } const FuncForwardRefComponent = forwardRef<FuncForwardRefComponentRef>((props, ref) => { /* в этом случае массив зависимостей необходим, без него будет при обновлении компонента каждый раз создаваться новый onClick, который в свою очередь заставит обновляться MemoButton */ useImperativeHandle(ref, () => ({ onClick: () => { ... } }), []); ... }) const FuncComponent = () => { const ref = useRef<FuncForwardRefComponentRef>(); return ( <div> <FuncForwardRefComponent ref={ref} /> <MemoButton onClick={ref.current.click}>Мемоизированная кнопка</MemoButton> </div> ) }
Заключение
ref или reference, то есть ссылки позволяют получить доступ к dom элементам, а также к react компонентам. Доступ к dom элементам и классовым компонентам можно получить без проблем, достаточно передать в ref атрибут объект типа { current: null } и в свойство current будет записана ссылка на элемент. Или можно передать в атрибут функцию, первым аргументом в которую прилетит ссылка на элемент, нам останется только ее сохранить. При работе с функциональными компонентами используем те же способы работы с атрибутом ref, но потребуются дополнительные шаги. Функциональные компоненты нужно обернуть в forwardRef, так внутри функционального компонента мы получим второй аргумент ref и сможем его передать в нужный html элемент, либо этот ref нужно передать в useImperativeHandle, это специальный хук, который добавил в ref дополнительные свойства, что позволит поднимать из функционального компонента дополнительные данные.
Всех заинтересованных приглашаю на бесплатный урок курса React.js по теме «Создание быстрых сайтов с Astro.build». Узнать подробнее о курсе и зарегистрироваться на бесплатный урок можно по ссылке ниже.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/677208/
Добавить комментарий