eFusion 28 февраля 2021

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

Вокруг React Hooks так и вьются постоянные интриги и расследования. Разберемся, что за штуки такие – эти useCallback, useMemo и прочие.
⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

Перевод публикуется с сокращениями, автор оригинальной статьи Milu.

Хуки позволяют использовать состояние и другие функции жизненного цикла при использовании функциональных компонентов вместо классов. Это более простой способ инкапсулировать stateful-поведение и побочные эффекты в UI, используя при этом меньше кода и повышая читабельность.

Давайте начнем с объяснения того, что происходит при повторном рендеринге компонента, затем определим назначение useCallback и useMemo, а также когда уместно их использовать.

Что происходит, когда компонент повторно визуализируется?

React повторно визуализирует компоненты при каждом изменении состояния или props. Поскольку функции в JavaScript считаются объектами, созданные внутри функционального компонента React, они будут создаваться снова при каждом повторном рендеринге. Это следствие референциального равенства, которое означает, что два выглядящих совершенно одинаково объекта не являются одинаковыми, если они оба не указывают на один и тот же объект.

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

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

Для чего нужны useCallback и useMemo?

Если useCallback и useMemo используются правильно, они необходимы, чтобы предотвратить повторные рендеры и сделать код более эффективным.

Рассмотрим их структуру. Оба хука получают два параметра: функцию и массив зависимостей.

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

useCallback возвращает один и тот же экземпляр передаваемой функции (параметр 1) вместо создания нового при каждом повторном рендеринге компонента. Новый экземпляр передаваемой функции (параметр 1) может быть создан только при изменении массива зависимостей (параметр 2).

Рассмотрим пример: приложение, которое увеличивает первое значение и/или уменьшает второе, а также обновляет итоговый результат. Есть еще третье дополнительное значение, которое пользователь может обновить, однако это число не используется в вычислениях.

Если пользователь взаимодействует с дополнительным значением состояния, компонент будет повторно визуализировать создание новой копии функции additionResult, даже если extraVal в ней не используется. В этом примере мы реализуем useCallback, чтобы создать новую копию функции additionResult при условии, что firstVal или secondVal будут обновлены.

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

useMemo используется вместо того, чтобы возвращать невызванную функцию, как это делает useCallback – он работает с передаваемой функцией и возвращает результирующее значение только при изменении массива параметров. Другими словами, useMemo вызывает функцию только при необходимости и возвращает кэшированное значение для других визуализаций.

В этом примере мы реализовали приложение, которое принимает число и возвращает его факториал. Например, если передать число 5, программа использовала бы рекурсивную функцию для вычисления: 5! = 5*4*3*2*1 = 120. Здесь мы использовали хук useMemo, чтобы React занимался пересчетом только при изменении числа.

Когда их использовать?

Если вы подумываете о добавлении useCallback и useMemo в ваш компонент, не торопитесь. Они добавляют некоторую дополнительную сложность коду и ухудшают читабельность. Менеджмент производительности с помощью useCallback и useMemo обходится дорого и это не всегда оправдано.

Рассмотрите возможность использования useCallback/useMemo в следующих ситуациях:

  • обработка больших объемов данных;
  • работа с интерактивными графиками и диаграммами;
  • реализация анимации;
  • включение компонента в ленивую загрузку.

Резюме

Когда компонент повторно визуализируется, он создает новые экземпляры всех объектов, включая все функции в нем:

  • useCallback – кэширует экземпляр функции между визуализациями.
  • useMemo – кэширует значение между визуализациями.

useRef

Почему useRef() – особенный?

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

Как использовать useRef()?

Мы инициализируем useRef() и передаем в него начальное значение или инициализируем его пустым, а значения обновляем позже:

        const testRef = useRef(1)
    

useRef() хранит содержащий атрибут current объект, который хранит переданное значение. В нашем примере он будет содержать значение 1.

        testRef = { current: 1 }
    

Для чего его использовать?

Для управления фокусом, выделения текста или воспроизведения мультимедиа. Большинство элементов внутри документа имеют атрибут ref, который облегчает использование useRef для ссылки на элементы внутри HTML. В качестве примера рассмотрим тег HTML <input/>. Создадим значение useRef и передадим его в <input/> в качестве атрибута ref. Теперь можно изменять входной элемент с помощью нескольких функций, которые заставят <input/> фокусироваться или размываться.

Еще одно полезное использование useRef – сохранение предыдущего значения состояния. Рассмотрим пример: есть список из трех покемонов, и вам нужно выбрать любимого. Выберите любой вариант, и вы увидите свой предыдущий выбор внизу. Это возможно в результате использования useRef:

        const previousSelected = useRef()
    

Затем каждый раз, когда мы выбираем другой вариант, он отслеживается в функции changeSelection():

        previousSelected.current = favPokemon
    

Где обновлять значение useRef()?

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

Обновление значения ref считается побочным эффектом. Именно по этой причине необходимо обновить значение ref в обработчиках событий и эффектах, а не во время визуализации (если только вы не работаете с ленивой инициализацией). React docs предупреждает нас, что несоблюдение этого правила может привести к неожиданному поведению приложения.

Использовать ли refs вместо state?

Нет. Refs – нереактивный, а значит, изменения значений не приведет к обновлению HTML. Взглянем на следующий пример. Мы инициализировали state, ref и 1000$. Этот компонент позволяет вам тратить эту сумму доллар за долларом каждый раз, когда нажимается кнопка «Spend».

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

Теперь, если вы потратите хранящиеся в ref средства, они будут вычитаться за каждый клик, но это изменение не вызовет повторного рендеринга – вы не увидите изменений на странице.

Вы можете проверить консоль, чтобы увидеть все значения ref:

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

useRef() аналог createRef?

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

useRef() полезен для доступа к узлам DOM или элементам React. Он сохраняет значение даже при повторной визуализации компонента. Вот пример, который позволит увидеть разницу. Мы инициализируем два значения, используя createRef и useRef. Каждый раз, когда нажимается кнопка «Add a render!», обновляется состояние renderCounter и вызывается повторная визуализация, в процессе которой проверяется, являются ли значения refs нулевыми. Если да, то присваиваем ему текущее значение состояния renderCounter.

Обратите внимание, что созданное с помощью useRef значение ref равно null только при первом рендеринге, поэтому оно устанавливается в 1 единственный раз.

С другой стороны, созданное с помощью createRef значение ref создается при каждом рендеринге, поэтому оно всегда null, а затем приравнивается к текущему значению состояния renderCounter.

Резюме

useRef() помогает создавать изменяемые переменные внутри функционального компонента, которые не будут обновляться при каждом рендеринге.

  • refs полезны для доступа к узлам DOM или элементам React (то, что визуализируется) и для хранения значений между визуализациями;
  • useRef() не следует использовать для замены состояния, потому что она нереактивная и не вызовет повторную визуализацию;
  • refs должны обновляться внутри эффектов и обработчиков событий, чтобы избежать странного поведения.

useContext

Пробрасывание props-ов vsContext

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все

React предоставляет нам поток данных, в котором родительский компонент использует props для обмена информацией со своими дочерними компонентами. Этот способ отслеживания данных отлично подходит для небольших приложений, однако по мере роста проекта вы можете обнаружить, что происходит проброс props через несколько слоев компонентов. Это называется prop drilling.

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

Альтернативой prop drilling является использование Context – простого решения, которое дает возможность доступа к данным между компонентами, даже если они не имеют отношений родитель-потомок.

Что такое context object?

Context object создается с помощью API createContext() и состоит из двух элементов: провайдер и потребитель. Чтобы создать объект контекста, вы можете инициализировать его пустым или со значением:

        const testContext = createContext();
    

Можно также получить доступ к его элементам:

        const testContext = createContext();
    

Как использовать провайдер?

Провайдер в context object должен быть обернут вокруг родительского элемента дерева компонентов – это дает каждому компоненту доступ к вашим глобальным данным. Взгляните на теги <Provider> – они делают состояние name доступным для всех обернутых компонентов. Теперь компоненты <NameModifier /> и <NamePrinter /> (и любой из их дочерних элементов) имеют доступ к name состояния, даже если мы не передаем name в качестве props.

        const App = () => {
  const { Provider } = testContext;
  const [name, setTestName] = useState(“Milu”);

  return (
    <Provider value={{ name }}>
      <NameModifier />
      <NamePrinter />
    </Provider>
  );
};
    

Как получить доступ к глобальным данным с помощью useContext()?

useContext() принимает объект контекста и возвращает текущие значения, доступные в качестве статических переменных. Здесь у нас есть компонент <NamePrinter / >, обернутый тегом Provider и мы можем получить доступ к значению name.

        export const NamePrinter = () => {
    const { name }  = useContext(testContext);

    return <div>My name is {name}!</div>
};
    

Как обновить контекст?

Сделать функции доступными можно и с помощью провайдера. В следующем примере создается функция updateName(), позволяющая изменять состояние имени. Если взглянуть на компонент <NameModifier />, там обращение к функции updateName() происходит через useContext и вызывается она каждый раз, при изменении данных.

Производительность

Использующий useContext() компонент будет повторно визуализироваться при обновлении значения в context object. Вы можете столкнуться с экземпляром, где одно из значений в контексте меняется очень часто, что может привести к повторной визуализации всех использующих useContext() компонентов, даже если быстро меняющееся значение используется только в небольшом дереве компонентов.

Рекомендуемое решение – разделить Context. Поэтому если у вас есть темы Light/Dark и переключатель для их выбора, необходимо создать ThemeContext и AppContext, как показано ниже:

        const App = ({ user, theme, themeToggle }) => {

  return (
    <ThemeProvider value={{ theme, themeToggle }}>
      <AppContext value={{ user }}>
        <HomePage />
      </AppContext>
    </ThemeProvider>
  );
};
    

Резюме

Использование context object – отличная альтернатива prop drilling. Он позволяет получить доступ к глобальным данным, не передавая их в качестве props.

  • context object содержит два элемента: провайдер и потребитель;
  • provider должен обернуть дерево компонентов;
  • useContext() позволяет получить доступ к глобальным данным из любых дочерних компонентов в дереве компонентов под оболочкой поставщика.
  • чтобы избежать повторных рендеров, разделите свой контекст – используйте ThemeContext и AppContext.

useReducer

Что такое reducer?

⚛ Демистификация хуков React: useCallback, useMemo и все-все-все
Reducer – это событие, которое будет выполнено, чтобы получить только одно значение. Возвращаемое значение может быть числом, строкой, массивом или даже объектом, если это оно единственное. Кроме того, reducer возвращают новое значение, а не мутируют начальное.

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

Мы применяем метод reduce к массиву чисел nums = [1,2,3,4,5]. Метод принимает два параметра:

reducer – функция, предоставляющая инструкции для получения одного значения. В нашем случае, для суммирования всех заданных значений в массиве nums.

        const reducer = (accumulator, currentValue) => accumulator + currentValue;
    

initialValue – начальное значение при реализации инструкций функции reducer. В нашем примере мы определяем начальное значение как 0, поэтому общее возвращаемое значение отражает только сумму значений в массиве nums.

        const initialValue = 0;
    

Теперь посмотрим на все это вместе. Метод reduce принимает initialValue и строит его, следуя приведенным в функции reducer инструкциям и добавляя каждое значение в массив nums до тех пор, пока не сможет вернуть одно общее значение.

        const reducer = (accumulator, currentValue) => accumulator + currentValue;

const nums = [1,2,3,4,5];

const initialValue = 0;

const totalValue = nums.reduce(reducer, initialValue);
    

Что такое useReducer()?

Хук useReducer используется для управления состоянием. В нем есть следующие параметры:

reducer – предоставляющую инструкции по управлению состоянием функция, которая принимает параметры state и action и возвращает новое состояние.

        (state, action) => newState
    

initialState – значение начальной точки. Оно будет меняться в соответствии с инструкциями reducer.

Похоже на описанное ранее поведение функции reduce, но хук useReducer не возвращает только одно значение. Вместо этого он возвращает два элемента в виде массива: текущее состояние и функцию отправки.

        const [state, dispatch] = useReducer(reducer, initialState);
    

Когда следует использовать useReducer?

useReducer является предпочтительной альтернативой useState в следующих случаях:

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

Резюме

Метод reduce полезен для получения одного значения после применения некоторой логики к группе значений.

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

Заключение

Мы рассмотрели очень обширную и интересную тему, требующую большой практики и дополнительного изучения. Рекомендуем ознакомиться с официальной документацией и продолжать обучение. Удачи!

Дополнительные материалы:

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ