React hooks, как не выстрелить себе в ноги. Часть 4

от автора

Использование ref в функциональных компонентах играет две роли:

  1. С помощью них можно получить ссылку на dom элемент

  2. 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>>;

Она может показаться страшной, на практике же, изменяется всего две вещи:

  1. forwardRef это дженерик и ему нужно передать 2 типа: тип ref и тип props, подробнее ниже.

  2. У функционального компонента появляется второй аргумент — 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *