Умный способ использования хука useRef() в React

9732
Умный способ использования хука useRef() в React
Умный способ использования хука useRef() в React

Краткое резюме ↬ В React компоненте useState и useReducer могут привести к тому, что ваш компонент будет перерисовываться при каждом вызове функций обновления. В этой статье вы узнаете, как использовать хук useRef() для отслеживания переменных, не вызывая повторного рендеринга, и как принудительно заставить React Components перерисовываться.

В компонентах React бывают случаи, когда необходимо отслеживать частые изменения без принудительного повторного рендеринга компонента. Также может возникнуть необходимость в эффективном повторном отображении компонента. Хотя хуки useState и useReducer являются API React для управления локальным состоянием в компоненте React, их использование может быть сопряжено со слишком частыми вызовами, заставляющими компонент перерисовываться при каждом вызове функций обновления.

В этой статье я объясню, почему useState неэффективно для отслеживания некоторых состояний, проиллюстрирую, как useState создает слишком частое повторное отображение компонента, как значения, хранящиеся в переменной, не сохраняются в компоненте, и, наконец, как useRef можно использовать для отслеживания переменных, не вызывая повторного отображения компонента. И дам решение, как обеспечить повторный рендеринг без ущерба для производительности компонента.

После появления функциональных компонентов, функциональные компоненты получили возможность иметь локальное состояние, которое вызывает повторный рендеринг компонента, как только происходит обновление любого из их локальных состояний.

function Card (props) {
  const [toggled, setToggled] = useState(false);
  
  const handleToggleBody  = () => {
    setToggled(!toggled)
  }
  
  return (<section className="card">
    <h3 className="card__title" onMouseMove={handleToggleBody}>
       {props.title}
    </h3>
    
    {toggled && <article className="card__body">
      {props.body}
    </article>}
  </section>)
}

// Consumed as:
<Card name="something" body="very very interesting" />

В компоненте выше карточка отображается с помощью элемента section , имеющего дочерний h3 класс card__title  который содержит заголовок карточки, а тело карточки отображается в теге article с телом card__body. Мы полагаемся на свойстваtitle и body, чтобы установить содержимое заголовка и тела карточки, в то время как body переключается только при наведении на заголовок.

РЕНДЕРИНГ КОМПОНЕНТА с useState #

Первоначальный рендеринг компонента выполняется, когда компонент имеет свои чистые, неразбавленные значения состояния, как и компонент Card, его первоначальный рендеринг выполняется, когда событие mouseover еще не сработало. Повторный рендеринг компонента выполняется в компоненте, когда одно из его локальных состояний или свойств было обновлено, это заставляет компонент вызвать свой метод рендеринга для отображения последних элементов на основе обновления состояния.

В компоненте Card обработчик события mousemoveвызывает функцию handleToggleBody для обновления состояния, отрицая предыдущее значение переключенного состояния.

Мы можем видеть этот сценарий в функции handleToggleBody, вызывающей функцию обновления состояния setToggled. Это приводит к тому, что функция вызывается каждый раз, когда срабатывает событие.

ХРАНЕНИЕ ЗНАЧЕНИЙ СОСТОЯНИЯ В ПЕРЕМЕННОЙ #

Обходным решением для повторного рендеринга является использование локальной переменной внутри компонента для хранения переключенного состояния, которое также может быть обновлено, чтобы предотвратить частый рендеринг — который выполняется только при обновлении локальных состояний или свойств компонента.

function Card (props) {
  let toggled = false;
  
  const handleToggleBody  = () => {
    toggled = !toggled;
    console.log(toggled);
  }
  
  return (<section className="card">
    <section className="cardTitle" onMouseMove={handleToggleBody}>
       {title}
    </section>
    
    {toggled && <article className="cardBody">
      {body}
    </article>}
  </section>)
}

<Card name="something" body="very very interesting" />

Это приводит к неожиданному поведению, когда значение обновляется, но компонент не перерисовывается, потому что не изменилось внутреннее состояние или свойства, чтобы вызвать перерисовку компонента.

ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ НЕ СОХРАНЯЮТСЯ ПРИ ПОВТОРНОМ РЕНДЕРИНГЕ #

Давайте рассмотрим шаги от первоначального рендеринга до повторного рендеринга компонента React.

  • Первоначально компонент инициализирует все переменные значениями по умолчанию, а также сохраняет все состояния и ссылки в уникальном хранилище, как определено алгоритмом React.
  • Когда новое обновление доступно для компонента через обновление его props или состояния, React извлекает старое значение для состояний и refs из своего хранилища и повторно инициализирует состояние старым значением, также применяя обновление к состояниям и refs, которые имеют обновление.
  • Затем он запускает функцию для компонента, чтобы перерисовать компонент с обновленными состояниями и refs. При повторном рендеринге переменные также будут заново инициализированы, чтобы иметь свои начальные значения, определенные в компоненте, поскольку они не отслеживаются.
  • Затем компонент перерисовывается.

Ниже приведен пример, который может это проиллюстрировать:

function Card (props) {
  let toggled = false;
  
  const handleToggleBody = () => {
    toggled = true;
    console.log(toggled);
  };

  useEffect(() => {
    console.log(“Component rendered, the value of toggled is:“, toggled);
  }, [props.title]);

  return (
    <section className=“card”>
      <h3 className=“card__title” onMouseMove={handleToggleBody}>
        {props.title}
      </h3>

      {toggled && <article className=“card__body”>{props.body}</article>}
    </section>
  );
}

// Renders the application
function App () {
  
  const [cardDetails, setCardDetails] = useState({
    title: “Something”,
    body: “uniquely done”,
  });

  useEffect(() => {
    setTimeout(() => {
      setCardDetails({
        title: “We”,
        body: “have updated something nice”,
      });
    }, 5000); // Force an update after 5s
  }, []);

  return (
    <div>
      <Card title={cardDetails.title} body={cardDetails.body} />
    </div>
  );
}

В приведенном выше коде  Card отображается как дочерний в компоненте App. Компонент App полагается на внутренний объект состояния cardDetails для хранения деталей карты. Кроме того, компонент обновляет состояние cardDetails через 5 секунд после первоначального рендеринга, чтобы заставить повторно отобразить список компонентов Card.

Card  имеет несколько измененное поведение; вместо переключения состояния toggled , оно устанавливается в true, когда курсор мыши помещается на заголовок карточки. Также используется хук useEffectдля отслеживания значения переменной toggled  после повторного рендеринга.

Использование переменной вместо state (второй тест)
Использование переменной вместо state (второй тест)

В результате после выполнения этого кода и наведения мыши на заголовок переменная обновляется внутри, но не вызывает повторного рендеринга, в то время как повторный рендеринг запускается родительским компонентом, который повторно инициализирует переменную в начальное состояние  false, как определено в компоненте. Интересно!

О useRef() Hook

Доступ к элементам DOM является ядром JavaScript в браузере, используя vanilla JavaScript, к элементу  div с классом  "title" можно получить доступ с помощью:

<div class="title">
  This is a title of a div
</div>
<script>
  const titleDiv = document.querySelector("div.title")
</script>

Ссылку на элемент можно использовать для таких интересных вещей, как изменение текстового содержимого  titleDiv.textContent = "this is a newer title" или изменение имени класса  titleDiv.classList = "This is the class" и многих других операций.

Со временем библиотеки манипулирования DOM, такие как jQuery, сделали этот процесс простым с помощью одного вызова функции с использованием знака $. Получить тот же элемент с помощью jQuery можно через  const el = ("div.title"), также текстовое содержимое может быть обновлено через API jQuery:  el.text("New text for the title div").

REFS в REACT через useRef HOOK

ReactJS, будучи современной фронтенд-библиотекой, пошла дальше, предоставив Ref API для доступа к своему элементу, и даже на шаг дальше — через хук  useRef  для функционального компонента.

import React, {useRef, useEffect} from "react";

export default function (props) {
  // Initialized a hook to hold the reference to the title div.
  const titleRef = useRef();
  
  useEffect(function () {
    setTimeout(() => {
      titleRef.current.textContent = "Updated Text"
    }, 2000); // Update the content of the element after 2seconds 
  }, []);
  
  return <div className="container">
    {/** The reference to the element happens here **/ }
    <div className="title" ref={titleRef}>Original title</div>
  </div>
}
Использование Ref для хранения состояния
Использование Ref для хранения состояния

Как видно выше, через 2 секунды после первоначального рендеринга компонента текстовое содержимое элемента  div с className title меняется на «Обновленный текст».

КАК ХРАНЯТСЯ ЗНАЧЕНИЯ В useRef

Переменная Ref в React — это изменяемый объект, но его значение сохраняется в React при всех повторных рендерингах. Объект ref имеет единственное свойство current, благодаря чему ref имеет структуру, подобную  { current: ReactElementReference}.

Решение команды React сделать ссылки постоянными и изменяемыми следует считать мудрым. Например, при перерисовке компонента элемент DOM может быть обновлен в процессе, тогда необходимо, чтобы ссылка на элемент DOM также была обновлена, а если не обновлена, то ссылка должна быть сохранена. Это помогает избежать несоответствий в окончательном рендеринге.

ИСКЛЮЧИТЕЛЬНО ОБНОВЛЕНИЕ ЗНАЧЕНИЯ переменной useRef

Обновление переменной  useRef, новое значение может быть присвоено .current  переменной ref. Это следует делать с осторожностью, если переменная ref ссылается на элемент DOM, что может привести к неожиданному поведению, в остальном же обновление переменной ref безопасно.

function User() {
  const name = useRef("Aleem");

  useEffect(() => {
    setTimeout(() => {
      name.current = "Isiaka";
      console.log(name);
    }, 5000);
  });

  return <div>{name.current}</div>;
}

Хранение значений в useRef

Уникальный способ применения хука useRef  — использовать его для хранения значений вместо ссылок DOM. Эти значения могут быть либо состоянием, которое не нужно менять слишком часто, либо состоянием, которое должно меняться как можно чаще, но не должно вызывать полного перерисовки компонента.

Возвращаясь к примеру с картой, вместо хранения значений в виде состояния или переменной используется ссылка.

function Card (props) {
  
  let toggled = useRef(false);
  
  const handleToggleBody  = () => {
    toggled.current = !toggled.current;
  }
  
  return (
    <section className=“card”>
      <h3 className=“card__title” onMouseMove={handleToggleBody}>
        {props.title}
      </h3>

      {toggled && <article className=“card__body”>{props.body}</article>}
    </section>
  );
  </section>)
}

Этот код дает желаемый результат внутренне, но не визуально. Значение переключенного состояния сохраняется, но при обновлении не происходит повторного рендеринга. Это происходит потому, что ожидается, что ссылки будут иметь одинаковые значения на протяжении всего жизненного цикла компонента, React не ожидает их изменения.

ПОВЕРХНОСТНЫЙ И ГЛУБОКИЙ РЕНДЕРИНГ

В React существует два механизма рендеринга: shallow и deep . Поверхностный рендеринг затрагивает только компонент, но не его дочерние элементы, в то время как глубокий рендеринг затрагивает сам компонент и все его дочерние элементы.

Когда происходит обновление ссылки, механизм поверхностного рендеринга используется для повторного рендеринга компонента.

function UserAvatar (props) {
  return <img src={props.src} />
}

function Username (props) {
  return <span>{props.name}</span>
}

function User () {
  const user = useRef({
    name: "Aleem Isiaka",
    avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
  })

  console.log("Original Name", user.current.name);
  console.log("Original Avatar URL", user.current.avatarURL);
  
  useEffect(() => {
    setTimeout(() => {
      user.current = {
        name: "Isiaka Aleem",
        avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
      };
    },5000)
  })
  
  // Both children won't be re-rendered due to shallow rendering mechanism
  // implemented for useRef
  return (<div>
    <Username name={user.name} />
      <UserAvatar src={user.avatarURL} />
  </div>);
}

В приведенном выше примере данные о пользователе хранятся в ссылке, которая обновляется через 5 секунд, компонент User имеет два дочерних элемента, Username для отображения имени пользователя и  UserAvatar  для отображения изображения аватара пользователя.

После обновления значение  useRef обновляется, но дочерние компоненты не обновляют свой пользовательский интерфейс, поскольку они не перерисовываются. Это поверхностный рендеринг, и именно он реализован для хука useRef.

Поверхностный рендеринг
Поверхностный рендеринг

Глубокий рендеринг используется при обновлении состояния с помощью хука  useState или обновлении реквизитов компонента.

function UserAvatar (props) {
  return <img src={props.src} />
}

function Username (props) {
  return <span>{props.name}</span>
}

function User () {
  const [user, setUser] = useState({
    name: "Aleem Isiaka",
    avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
  });

  useEffect(() => {
    setTimeout(() => {
      setUser({
        name: "Isiaka Aleem",
        avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
      });
    },5000);
  })
  
  // Both children are re-rendered due to deep rendering mechanism
  // implemented for useState hook
  return (<div>
    <Username name={user.name} />
      <UserAvatar src={user.avatarURL} />
  </div>);
}
Глубокий рендеринг
Глубокий рендеринг

В отличие от результата, наблюдаемого при использовании  useRef , дочерние элементы в данном случае получают последнее значение и перерисовываются, что позволяет добиться желаемых эффектов в их пользовательских интерфейсах.

ФОРСИРОВАНИЕ ГЛУБОКОГО РЕНДЕРА ПРИ ОБНОВЛЕНИИ useRef

Чтобы добиться глубокого рендеринга при обновлении рефссылки, можно частично реализовать механизм глубокого рендеринга хука  useState.

function UserAvatar (props) {
  return <img src={props.src} />
}

function Username (props) {
  return <span>{props.name}</span>
}

function User () {
  const user = useRef({
    name: "Aleem Isiaka",
    avatarURL: "https://icotar.com/avatar/jake.png?bg=e91e63",
  })

  const [, setForceUpdate] = useState(Date.now());
  
  useEffect(() => {
    setTimeout(() => {
      user.current = {
        name: "Isiaka Aleem",
        avatarURL: "https://icotar.com/avatar/craig.png?s=50", // a new image
      };
      
      setForceUpdate();
    },5000)
  })
  return (<div>
    <Username name={user.name} />
      <UserAvatar src={user.avatarURL} />
  </div>);
}
Глубокий + поверхностный рендеринг
Глубокий + поверхностный рендеринг

В приведенном выше улучшении компонента  User вводится состояние, но его значение игнорируется, так как оно не требуется, а функция обновления для принудительной перерисовки компонента называется  setForceUpdate, чтобы сохранить соглашение об именовании для хука  useState. Компонент ведет себя так, как и ожидалось, и перерисовывает дочерние элементы после обновления ссылки.

Это может вызвать такие вопросы, как:

«Не является ли это анти-паттерном?»

«Разве это не то же самое, что и исходная проблема, а по-другому?»

Конечно, это анти-паттерн, потому что мы используем гибкость хука  useRef для хранения локальных состояний, и все еще вызываем хук  useState, чтобы убедиться, что дочерние элементы получают последнее значение текущего значения переменной  useRef, и оба эти действия могут быть достигнуты с помощью  useState.

Да, это делает почти то же самое, что и в исходном случае, но по-другому. Функция setForceUpdate  выполняет глубокий рендеринг, но не обновляет состояние, действующее на элемент компонента, что сохраняет его последовательность при всех повторных рендерах.

Заключение

Частое обновление состояния в компоненте React с помощью хука  useState может привести к нежелательным последствиям. Мы также видели, что хотя переменные могут быть лучшим вариантом, они не сохраняются при перерисовке компонента, как сохраняется состояние.

Ссылки в React используются для хранения ссылки на элемент React, и их значения сохраняются при перерисовке. Ссылки — это изменяемые объекты, поэтому они могут быть обновлены явно и могут хранить значения, отличные от ссылки на элемент React.

Хранение значений в Refs решает проблему частого перерисовки, но создает новую проблему — компонент не обновляется после изменения значения Refs, которую можно решить, введя функцию обновления состояния setForceUpdate.

В целом, здесь можно сделать следующие выводы:

  • Мы можем хранить значения в refs и обновлять их, что более эффективно, чем useState, который может быть дорогим, если значения должны обновляться несколько раз в течение секунды.
  • Мы можем заставить React повторно отобразить компонент, даже если обновление не требуется, используя функцию обновления useStateбез ссылок.
  • Мы можем объединить 1 и 2, чтобы получить высокопроизводительный постоянно меняющийся компонент.