Экскурсия по двум самым известным хукам в React
Если вы изо всех сил пытались разобраться в useMemo и useCallback, вы не одиноки! Я разговаривал со многими разработчиками React, которые cломали голову над этими двумя хуками.
Моя цель в этом здесь — прояснить всю эту путаницу. Мы узнаем, что они делают, почему они полезны и как получить от них максимальную пользу.
Погнали!
Целевая аудитория
Это руководство написано, чтобы помочь начинающим/мидл разработчикам освоиться с React. Если вы делаете свои первые шаги в React, добавьте этот шаг в закладки и вернитесь к нему через несколько недель!
Основная идея
Окей, давайте начнем с useMemo.
Основная идея useMemo заключается в том, что он позволяет нам «запоминать» вычисленное значение между рендерами.
Это определение требует некоторой расшифровки. На самом деле, для этого требуется довольно сложная мысленная модель того, как работает React- давайте сначала разберемся с этим.
Главное, что делает React, — это синхронизирует наш интерфейс с состоянием нашего приложения. Инструмент, который он использует для этого, называется « Каждый «ре-рендеринг» создает мысленную картину того, как должен выглядеть DOM, исходя из текущего состояния. На картинке выше он изображен в виде HTML, но на самом деле это набор объектов JS. Вы наверняка слышали этот термин, его иногда называют «виртуальным DOM». Мы не говорим React напрямую, какие узлы DOM необходимо изменить. Вместо этого мы сообщаем React, каким должен быть интерфейс, исходя из текущего состояния. При повторном рендеринге React создает новый снимок и может определить, что нужно изменить, сравнивая снимки, как в игре «найди 10 отличий». React изначально мощно оптимизирован, поэтому в целом повторный рендеринг не имеет большого значения. Но в некоторых ситуациях создание таких снимков занимает некоторое время. Это может привести к проблемам с производительностью, например, интерфейс не обновляется достаточно быстро после выполнения действия пользователем. Грубо говоря, Уменьшение объема работы, которую необходимо выполнить при каждом рендеринге. Уменьшение количества раз, когда компонент вообще необходимо перерисовывать. Давайте обсудим эти случаи по отдельности. Предположим, мы создаем инструмент, который поможет пользователям найти все простые числа от 0 до Вот одна из возможных реализаций: Я не ожидаю, что вы прочтете здесь каждую строку кода, поэтому вот основные моменты: У нас есть одно состояние, число под названием Используя цикл Мы визуализируем ввод числа, чтобы пользователь мог изменить Мы показываем пользователю все простые числа, которые мы вычислили. Этот код требует значительного объема вычислений. Если пользователь выберет большое значение Иногда нам действительно необходимо выполнять этот расчет, например, когда пользователь выбирает новый Например, предположим, что в нашем примере также есть цифровые часы: Наше приложение теперь имеет два состояния: Вот в чем проблема: всякий раз, когда меняется какая-либо из переменных состояния, мы заново запускаем все эти дорогостоящие вычисления простых чисел. А поскольку время меняется раз в секунду, это означает, что мы постоянно заново генерируем этот список простых чисел, даже если выбранное пользователем число не изменилось! В JavaScript у нас есть только один основной поток, и мы постоянно его нагружаем, запуская этот код снова и снова, каждую секунду. Это означает, что приложение может работать медленно, когда пользователь пытается делать другие вещи, особенно на устройствах более низкого уровня. Но что, если бы мы могли «пропустить» эти вычисления? Если у нас уже есть список простых чисел для данного числа, почему бы не использовать это значение повторно вместо того, чтобы каждый раз вычислять его с нуля? Именно это позволяет нам сделать Кусок кода, который необходимо выполнить, завернутый в функцию Список зависимостей Во время монтирования, когда этот компонент отображается в первый раз, React вызывает эту функцию для запуска всей логики, вычисляя простые числа. Все, что мы возвращаем из этой функции, присваивается переменной Теперь для каждого последующего рендеринга у React есть выбор: Вызвать функцию еще раз, чтобы пересчитать значение, или Повторно использовать данные с момента последнего выполнения. Чтобы ответить на этот вопрос, React просматривает предоставленный список зависимостей. Изменилось ли что-нибудь из них со времени предыдущего рендера? Если да, React перезапустит функцию, чтобы вычислить новое значение. В противном случае вся эта работа будет пропущена и будет повторно использовано ранее вычисленное значение. В этом случае мы, по сути, говорим: «пересчитывать список простых чисел только при изменении Это широко известно как мемоизация, и именно поэтому этот хук называется « Вот рабочая версия этого решения: Итак, хук Часто мы можем избежать необходимости использования useMemo путем реструктуризации нашего приложения. Вот один из способов: Я извлек два новых компонента: Мы много слышим о поднятии состояния, но иногда лучший подход — передать наш стэйт вниз! Каждый компонент должен нести одну ответственность, и в примере выше приложение делало две совершенно несвязанные вещи. Это не всегда будет возможным. В большом реальном приложении существует множество состояний, которые необходимо поднять довольно высоко и которые нельзя опустить. У меня есть еще один трюк на этот случай. Давайте посмотрим на пример. Предположим, нам нужно, чтобы переменная времени была поднята выше Подобно силовому полю, Это так называемый чистый компонент. По сути, мы сообщаем React, что этот компонент всегда будет выдавать один и тот же результат при одних и тех же входных данных, и мы можем пропустить повторные рендеринги, в которых ничего не изменилось. В примере выше я применяю По правде говоря, это немного необычно. Я решил структурировать его так, чтобы все было видно в одном файле и было легче понять. На практике я часто применяю Наш компонент Если нам когда-нибудь понадобится «нечистая» версия Здесь интересный сдвиг перспективы: раньше мы запоминали результат определенного вычисления, однако в данном случае я запомнил весь компонент. В любом случае, дорогостоящие вычисления будут выполняться повторно только тогда, когда пользователь выберет новый Я не утверждаю, что один подход лучше другого — каждому инструменту есть свое место. Но в данном конкретном случае я предпочитаю именно такой подход. Теперь, если вы когда-либо пытались использовать чистые компоненты в реальных условиях, вы, вероятно, заметили одну особенность: чистые компоненты часто перерисовываются, даже если кажется, что ничего не изменилось! Это плавно подводит нас ко второй проблеме, которую решает В примере ниже я создал компонент А также несвязанное состояние — имя пользователя. И тем не менее, всякий раз, когда пользователь меняет свое имя, Boxes также перерисовывается! Что за черт?! Почему силовое поле Компонент Вот в чем проблема: каждый раз, когда React выполняет повторный рендеринг, мы создаем совершенно новый массив. Они эквивалентны с точки данных, но не с точки зрения ссылки. Я думаю, будет полезно на секунду забыть о React и поговорить о старом добром JavaScript. Давайте рассмотрим аналогичную ситуацию: Что вы думаете? Равен ли первый результат второму? В каком-то смысле так оно и есть. Обе переменные имеют одинаковую структуру Оператор Мы создали два разных массива. Они могут содержать одно и то же содержимое, но не являются одним и тем же массивом, точно так же, как два идентичных близнеца не являются одним и тем же человеком. Каждый раз, когда мы вызываем функцию Обратите внимание, что простые типы данных — такие как строки, числа и логические значения — можно сравнивать по значению. Но когда дело доходит до массивов и объектов, они сравниваются только по ссылке. Возвращаясь к React: наш компонент Когда состояние И Структура массива Чтобы решить эту проблему, мы можем использовать хук В отличие от примера с простыми числами, здесь нас не беспокоят дорогостоящие вычисления. Наша единственная цель — сохранить ссылку на конкретный массив. Мы указываем Думаю, небольшой набросок поможет проиллюстрировать это. Раньше мы создавали новый массив как часть каждого снимка: Однако с помощью Сохраняя одну и ту же ссылку при нескольких рендерах, мы позволяем чистым компонентам функционировать так, как мы хотим, игнорируя рендеры, которые не влияют на интерфейс. Окей, это всё касалось Вот короткая версия: это то же самое, но для функций вместо массивов/объектов. Подобно массивам и объектам, функции сравниваются по ссылке, а не по значению: Это означает, что если мы определим функцию внутри наших компонентов, она будет заново генерироваться при каждом рендере, каждый раз создавая идентичную, но уникальную функцию. Давайте посмотрим на пример: Этот SandBox изображает типичное приложение-счетчик, но со специальной кнопкой «Mega Boost». Эта кнопка значительно увеличивает счетчик, на тот случай, если вы спешите и не хотите нажимать стандартную кнопку несколько раз. Компонент Как мы видели в случае с массивом Используя то, что мы узнали об Вместо возврата массива мы возвращаем функцию. Эта функция затем сохраняется в переменной Это работает… но есть способ получше: Другими словами, эти два выражения имеют одинаковый эффект: Хорошо, мы увидели, как По моему личному мнению, было бы пустой тратой времени заключать в эти хуки каждый отдельный объект/массив/функцию. В большинстве случаев выгоды незначительны; React высоко оптимизирован, и ре-рендеринг часто не такой медленный и дорогой, как мы часто думаем! Лучший способ использовать эти хуки — это реакция на проблему. Если вы заметили, что ваше приложение становится немного медленным, вы можете использовать React Profiler, чтобы отслеживать медленный рендеринг. В некоторых случаях производительность можно повысить путем реструктуризации приложения. В других случаях Тем не менее, есть несколько сценариев, в которых я заранее применяю эти хуки. Команда React активно исследует, возможно ли «авто-мемоизация» кода на этапе компиляции. Он все еще находится на стадии исследования, но ранние эксперименты кажутся многообещающими. Возможно, в будущем все это будет делаться за нас автоматически. Однако до тех пор нам все равно придется что-то оптимизировать самостоятельно. Один из моих любимых маленьких пользовательских хуков — Вот как определяется этот хук: Обратите внимание, что функция переключения запоминается с помощью Когда я создаю такие многоразовые хуки, мне нравится делать их максимально эффективными, потому что я не знаю, где они будут использоваться в будущем. В 95% ситуаций это может быть излишним, но если я использую этот хук 30 или 40 раз, есть большая вероятность, что это поможет улучшить производительность моего приложения. Когда мы делимся данными в приложении с помощью контекста, обычно в качестве атрибута передается большой объект. Рекомендую запомнить этот объект: Почему это выгодно? Могут существовать десятки чистых компонентов, использующих этот контекст. Без Уф! Вы дошли до конца. Я знаю, что этот урок охватывает довольно непростую тему. Я знаю, что эти два хука сложны, что сам React может показаться очень сложным и запутанным. Это сложный инструмент! Но вот в чем дело: если вы сможете преодолеть первоначальный барьер, использовать React будет сплошное удовольствие.
useMemo и useCallback — это инструменты, созданные для того, чтобы помочь нам оптимизировать ре-рендеринг. Они делают это двумя способами:
Use case 1: Тяжелые вычисления
selectedNum, где selectedNum — это значение, введенное пользователем. Простое число — это число, которое можно разделить только на 1 и себя, например 17.Код примера
import React from 'react'; function App() { // Мы сохраняем выбранный пользователем номер в состоянии. const [selectedNum, setSelectedNum] = React.useState(100); // Мы вычисляем все простые числа от 0 до выбранного пользователем // числа «selectedNum»: const allPrimes = []; for (let counter = 2; counter < selectedNum; counter++) { if (isPrime(counter)) { allPrimes.push(counter); } } return ( <> <form> <label htmlFor="num">Your number:</label> <input type="number" value={selectedNum} onChange={(event) => { // Чтобы компьютеры не взрывались, мы ограничимся 100 тысячами. let num = Math.min(100_000, Number(event.target.value)); setSelectedNum(num); }} /> </form> <p> There are {allPrimes.length} prime(s) between 1 and {selectedNum}: {' '} <span className="prime-list"> {allPrimes.join(', ')} </span> </p> </> ); } // Вспомогательная функция, которая определяет, // является ли данное число простым или нет. function isPrime(n){ const max = Math.ceil(Math.sqrt(n)); if (n === 2) { return true; } for (let counter = 2; counter <= max; counter++) { if (n % counter === 0) { return false; } } return true; } export default App;
selectedNum.for, мы вручную вычисляем все простые числа от 0 до selectedNum.selectedNum.selectedNum, нам придется просмотреть десятки тысяч чисел, проверяя, является ли каждое из них простым. И хотя существуют более эффективные алгоритмы проверки простых чисел, чем тот, который я использовал выше, они всегда будут требовать больших вычислительных ресурсов.selectedNum. Но мы потенциально можем столкнуться с некоторыми проблемами с производительностью, если будем выполнять эту работу безвозмездно, когда в этом нет необходимости.import format from 'date-fns/format'; function App() { //тот же код // `time` — это переменная состояния, которая меняется // раз в секунду, поэтому она всегда синхронизируется с текущим временем. const time = useTime(); //тот же код return ( <> <p className="clock"> {format(time, 'hh:mm:ss a')} </p> //... </> ); } function useTime() { const [time, setTime] = React.useState(new Date()); React.useEffect(() => { const intervalId = window.setInterval(() => { setTime(new Date()); }, 1000); return () => { window.clearInterval(intervalId); } }, []); return time; } //тот же код export default App;selectedNum и time. Раз в секунду переменная времени обновляется, отражая текущее время, и это значение используется для отображения часов в правом верхнем углу.
useMemo. Вот как это выглядит:const allPrimes = React.useMemo(() => { const result = []; for (let counter = 2; counter < selectedNum; counter++) { if (isPrime(counter)) { result.push(counter); } } return result; }, [selectedNum]);useMemo принимает два аргумента:
allPrimes.
useMemo по сути похож на небольшой кеш, а зависимости — это стратегия изменения этого кеша.selectedNum». Когда компонент ре-рендерится по другим причинам (например, из-за изменения состояния времени), useMemo игнорирует функцию и передает кэшированное значение.useMemo».Полная версия кода
import React from 'react'; import format from 'date-fns/format'; function App() { const [selectedNum, setSelectedNum] = React.useState(100); const time = useTime(); const allPrimes = React.useMemo(() => { const result = []; for (let counter = 2; counter < selectedNum; counter++) { if (isPrime(counter)) { result.push(counter); } } return result; }, [selectedNum]); return ( <> <p className="clock"> {format(time, 'hh:mm:ss a')} </p> <form> <label htmlFor="num">Your number:</label> <input type="number" value={selectedNum} onChange={(event) => { // To prevent computers from exploding, // we'll max out at 100k let num = Math.min(100_000, Number(event.target.value)); setSelectedNum(num); }} /> </form> <p> There are {allPrimes.length} prime(s) between 1 and {selectedNum}: {' '} <span className="prime-list"> {allPrimes.join(', ')} </span> </p> </> ); } function useTime() { const [time, setTime] = React.useState(new Date()); React.useEffect(() => { const intervalId = window.setInterval(() => { setTime(new Date()); }, 1000); return () => { window.clearInterval(intervalId); } }, []); return time; } function isPrime(n){ const max = Math.ceil(Math.sqrt(n)); if (n === 2) { return true; } for (let counter = 2; counter <= max; counter++) { if (n % counter === 0) { return false; } } return true; } export default App;Альтернативный подход
useMemo может помочь нам избежать ненужных вычислений… но действительно ли это лучшее решение?import React from 'react'; import Clock from './Clock'; import PrimeCalculator from './PrimeCalculator'; function App() { return ( <> <Clock /> <PrimeCalculator /> </> ); } export default App;Clock и PrimeCalculator. Отделившись от App, каждый из этих двух компонентов управляет своим собственным состоянием. Повторный рендеринг одного компонента не повлияет на другой.PrimeCalculator:import React from 'react'; import { getHours } from 'date-fns'; import Clock from './Clock'; import PrimeCalculator from './PrimeCalculator'; // Превращаем наш PrimeCalculator в чистый компонент: const PurePrimeCalculator = React.memo(PrimeCalculator); function App() { const time = useTime(); // Придумываем подходящий цвет фона, исходя из времени суток: const backgroundColor = getBackgroundColorFromTime(time); return ( <div style={{ backgroundColor }}> <Clock time={time} /> <PurePrimeCalculator /> </div> ); } const getBackgroundColorFromTime = (time) => { const hours = getHours(time); if (hours < 12) { // A light yellow for mornings return 'hsl(50deg 100% 90%)'; } else if (hours < 18) { // Dull blue in the afternoon return 'hsl(220deg 60% 92%)' } else { // Deeper blue at night return 'hsl(220deg 100% 80%)'; } } function useTime() { const [time, setTime] = React.useState(new Date()); React.useEffect(() => { const intervalId = window.setInterval(() => { setTime(new Date()); }, 1000); return () => { window.clearInterval(intervalId); } }, []); return time; } export default App;React.memo окружает наш компонент и защищает его от посторонних обновлений. Наш PurePrimeCalculator будет перерисовываться только при получении новых данных или при изменении его внутреннего состояния.Более традиционный подход
React.memo к импортированному компоненту PrimeCalculator.React.memo для экспорта компонентов, например:// PrimeCalculator.js function PrimeCalculator() { /* Наполнение компонента */ } export default React.memo(PrimeCalculator);PrimeCalculator теперь всегда будет «чистым», и нам не придется с ним возиться, когда мы соберёмся его использовать.PrimeCalculator, мы сможем экспортировать компонент как именованный экспорт. Хотя я не думаю, что мне когда-либо приходилось так делать.selectedNum. Но мы оптимизировали родительский компонент, а не отдельные медленные строки кода.useMemo.Use case 2: Сохраненные ссылки
Boxes. В нем отображен набор ярких коробок, которые можно использовать в каких-то декоративных целях.Код
App.jsimport React from 'react'; import Boxes from './Boxes'; function App() { const [name, setName] = React.useState(''); const [boxWidth, setBoxWidth] = React.useState(1); const id = React.useId(); // Попробуйте поменять какие-нибудь из этих значений! const boxes = [ { flex: boxWidth, background: 'hsl(345deg 100% 50%)' }, { flex: 3, background: 'hsl(260deg 100% 40%)' }, { flex: 1, background: 'hsl(50deg 100% 60%)' }, ]; return ( <> <Boxes boxes={boxes} /> <section> <label htmlFor={`${id}-name`}> Name: </label> <input id={`${id}-name`} type="text" value={name} onChange={(event) => { setName(event.target.value); }} /> <label htmlFor={`${id}-box-width`}> First box width: </label> <input id={`${id}-box-width`} type="range" min={1} max={5} step={0.01} value={boxWidth} onChange={(event) => { setBoxWidth(Number(event.target.value)); }} /> </section> </> ); } export default App;Boxes.jsimport React from 'react'; function Boxes({ boxes }) { return ( <div className="boxes-wrapper"> {boxes.map((boxStyles, index) => ( <div key={index} className="box" style={boxStyles} /> ))} </div> ); } export default React.memo(Boxes);Boxes — это чистый компонент благодаря использованию React.memo() вокруг экспорта по умолчанию в Boxes.js. Это означает, что он должен выполнять повторный рендеринг только при изменении реквизитов.React.memo() не защищает нас здесь??Boxes имеет только один проп — boxes, и создается впечатление, что мы передаем ему одни и те же данные при каждом рендеринге. Всегда одно и то же: красная коробка, широкая фиолетовая коробка, желтая коробка. У нас есть переменная состояния boxWidth, которая влияет на массив блоков, но мы ее не меняем!function getNumbers() { return [1, 2, 3]; } const firstResult = getNumbers(); const secondResult = getNumbers(); console.log(firstResult === secondResult);[1, 2, 3]. Но это не то, что на самом деле проверяет оператор ===.=== проверяет, являются ли два выражения одним и тем же.
getNumbers, мы создаем совершенно новый массив, отдельный объект, хранящийся в памяти компьютера. Если мы вызовем его несколько раз, мы сохраним в памяти несколько копий этого массива.Boxes также является функцией JavaScript. Когда мы его визуализируем, мы вызываем эту функцию:// Каждый раз, когда мы отображаем этот компонент, мы вызываем эту функцию... function App() { // ...и создаём совершенно новый массив... const boxes = [ { flex: boxWidth, background: 'hsl(345deg 100% 50%)' }, { flex: 3, background: 'hsl(260deg 100% 40%)' }, { flex: 1, background: 'hsl(50deg 100% 60%)' }, ]; // ...который затем передается в качестве аргумента этому компоненту! return ( <Boxes boxes={boxes} /> ); }name меняется, наш компонент выполняет ре-рендеринг, что повторно запускает весь код. Мы создаем новый массив boxes и передаем его в наш компонент Boxes.Boxes перерисовывается, потому что мы дали ему совершенно новый массив!boxes не менялась между рендерами, но это не имеет значения. Все, что знает React, это то, что проп boxes получил только что созданный, никогда ранее не виданный массив.useMemo:const boxes = React.useMemo(() => { return [ { flex: boxWidth, background: 'hsl(345deg 100% 50%)' }, { flex: 3, background: 'hsl(260deg 100% 40%)' }, { flex: 1, background: 'hsl(50deg 100% 60%)' }, ]; }, [boxWidth]);boxWidth как зависимость, потому что хотим, чтобы компонент Boxes ре-рендерился, когда пользователь настраивает ширину красного поля.
useMemo мы повторно используем ранее созданный массив блоков:
Хук useCallback
UseMemo… а что насчет useCallback?const functionOne = function() { return 5; }; const functionTwo = function() { return 5; }; console.log(functionOne === functionTwo); // falseimport React from 'react'; import MegaBoost from './MegaBoost'; function App() { const [count, setCount] = React.useState(0); function handleMegaBoost() { setCount((currentValue) => currentValue + 1234); } return ( <> Count: {count} <button onClick={() => { setCount(count + 1) }} > Click me! </button> <MegaBoost handleClick={handleMegaBoost} /> </> ); } export default App;MegaBoost является чистым компонентом благодаря React.memo. Это не зависит от счётчика… Но оно перерисовывается всякий раз, когда количество меняется!boxes, проблема здесь в том, что мы генерируем совершенно новую функцию при каждом рендере. Если мы отрисуем 3 раза, мы создадим 3 отдельные функции handleMegaBoost, прорываясь через силовое поле React.memo.useMemo, мы могли бы решить проблему следующим образом:const handleMegaBoost = React.useMemo(() => { return function() { setCount((currentValue) => currentValue + 1234); } }, []);handleMegaBoost.const handleMegaBoost = React.useCallback(() => { setCount((currentValue) => currentValue + 1234); }, []);useCallback служит той же цели, что и useMemo, но он создан специально для функций. Мы передаем ему функцию напрямую, и он запоминает эту функцию, распределяя ее между рендерами.// Это: React.useCallback(function helloWorld(){}, []); // ...Функционально эквивалентно этому: React.useMemo(() => function helloWorld(){}, []);
useCallback — это синтаксический сахар. Он существует исключительно для того, чтобы сделать нашу жизнь немного приятнее при запоминании функций колбэков.Когда использовать эти хуки
useMemo и useCallback позволяют нам прокидывать ссылки между несколькими рендерами, повторно использовать сложные вычисления или избегать нарушения работы чистых компонентов. Вопрос в том, как часто нам нужно их использовать?useMemo и useCallback могут помочь ускорить процесс.Это может измениться в будущем!
Внутри кастомных хуков
useToggle, помощник, который работает почти так же, как useState, но может только переключать состояние между true и false:function App() { const [isDarkMode, toggleDarkMode] = useToggle(false); return ( <button onClick={toggleDarkMode}> Toggle color theme </button> ); }function useToggle(initialValue) { const [value, setValue] = React.useState(initialValue); const toggle = React.useCallback(() => { setValue(v => !v); }, []); return [value, toggle]; }useCallback.В провайдерах внутреннего контекста
const AuthContext = React.createContext({}); function AuthProvider({ user, status, forgotPwLink, children }){ const memoizedValue = React.useMemo(() => { return { user, status, forgotPwLink, }; }, [user, status, forgotPwLink]); return ( <AuthContext.Provider value={memoizedValue}> {children} </AuthContext.Provider> ); }useMemo все эти компоненты будут вынуждены выполнить ре-рендер, если родительский элемент AuthProvider произведет ре-рендер.
ссылка на оригинал статьи https://habr.com/ru/articles/807139/
Добавить комментарий