Когда же нужно использовать useCallback
Все мы знаем, что с появлением React 16.8, в нашу жизнь пришли хуки. Действительно, они упрощают процесс написания кода, используя функциональные компоненты в сравнении с классами, но некоторые хуки вызывают много вопросов у разработчиком. В данной статье мы рассмотрим на примерах, когда же нужно использовать useCallback, а когда можно обойтись без него.
Мы знаем из документации, что useCallback возвращает мемоизированную версию коллбэка. Простыми словами возвращает одну и туже ссылку на функцию, до тех пор, пока не изменится одна из зависимостей.
const memoizedCallback = useCallback( () => < doSomething(a, b); >, [a, b], );
Давайте рассмотрим пример:
У нас имеется страница с инпутом, списком отображения элементов и кнопкой, которая добавляет в список введенный элемент. При клике на сам элемент списка, он будет удаляться. Дефолтный список возьмём из 5 элементов, который мы будем редактировать.
const listOfCities = ['Beijing','Tokyo','Kinshasa','Moscow','Jakarta']; const Page = () => < const [name, setName] = useState(""); const [list, setList] = useState(listOfCities); const handleClick = () =>< setList([. list, name]); setName(""); >; const handleChange = (event: React.ChangeEvent) => < setName(event.target.value); >; const handleRemoveClick = (item: string) => < const filteredList = list.filter((listItem) =>listItem !== item); setList(filteredList); >; console.log("Page render"); return ( onChange= /> onRemoveClick= /> ); >;
И компоненты отображения списка
export const CitiesList = () => < console.log("List render"); return list.map((item) => < return city= onRemoveClick= />; >); >; export const City = () => < const handleCityClick = () =>onRemoveClick(city); console.log("Element render"); return > ; >;
Итак, какие же есть сейчас проблемы с перформансом. Давайте запустим и посмотрим что у нас получилось, и сразу откроем консоль разработчика.
Кажется все логично, произошел рендер страницы, компонента списка и его 5 элементов.
Начинаем вводить в инпут символы и видим, что на каждый введенный символ, происходит аналогичный рендер во всех компонентах. Изменения значения инпут (state name), вызывает рендер во всех дочерних компонентах.
У нас есть несколько callback функции, которые теоретически можно обернуть в useCallback. Разберем их по отдельности.
- Обернув handleClick, handleChange или handleCityClick в useCallback, увы это никак не улучшит перфоманс, ведь useCallback это тоже функция, которая при каждом рендер, будет заново сравнивать зависимости и возвращать новую или старую ссылку на функцию.
- Обернув handleRemoveClick в useCallback, это уменьшит количество ререндеров при условии что мы обернем СitiesList в React.memo, подробнее о нем можно посмотреть в документации.
const handleRemoveClick = useCallback( (item: string) => < const filteredList = list.filter((listItem) =>listItem !== item); setList(filteredList); >, [list] );
const CitiesList = React.memo((< list, onRemoveClick >) => < console.log("List render"); return list.map((item) => < return item= onRemoveClick= />; >); >);
Теперь при вводе символов в инпуте, компонент СitiesList не перерендеривается. На список из 5 элементов улучшение перформанса будет не столь большим, но наглядно показывает как его можно будет улучшить в случае необходимости.
В данном примере можно было вынести инпут с кнопкой и его state в отдельный компонент, тогда необходимость использования useCallback уже была бы не актуальной, поэтому не делайте оптимизаций производительности до тех пор, пока это действительно не потребуется.
Какие из этого можно сделать вывод
Встроенный хук useCallback нужная и полезная вещь, которая помогает улучшить перформанс. Но её применение не всегда актуально. Используйте его для функций, которые передаются компонентам с большими затратами памяти для отображения. Делайте оптимизацию производительности после написания и рефакторинга кода.
Надеюсь, эта статья была вам полезной. Всем пока 🙂
- hooks
- usecallback
- производительность javascript
- использование useCallback
Хуки useCallback и useMemo — JS: React Hooks
В разработке приложений на React часто возникают ситуации, когда компоненты отрисовываются без необходимости. Чаще всего это происходит при отрисовке родительского компонента или изменении пропсов.
Лишняя перерисовка компонентов может привести к непредвиденным событиям, когда при отрисовке компонента срабатывают побочные эффекты, либо перерисовка может повлиять на производительность.
Самый простой способ отследить перерисовку компонента — это расставить точки остановки выполнения кода в дебагере, либо добавить вывод лога.
Ниже простое приложение из двух компонентов:
import useState, useEffect > from 'react'; const Greeting = () => useEffect(() => console.log(`Компонент Greeting отрисован в $new Date().toLocaleTimeString()>`); >); return h3>Hello, world!h3>; >; const App = () => const [name, setName] = useState(''); return ( <> input value=name> onChange=(e) => setName(e.target.value)> /> Greeting /> > ); >;
В компонент Greeting мы добавили вывод в console.log() с меткой о времени для наглядности. Так мы сможем отследить время отрисовки компонента. Компонент App использует в представлении Greeting и содержит поле ввода, в котором меняется состояние компонента.
Если ввести значение в input , то в консоли браузера мы увидим вывод сообщения об отрисовке компонента Greeting . Компонент будет отрисовываться каждый раз, когда мы вводим значение в поле. Добавляем или удаляем один символ — и компонент перерисовывается. Если мы напечатаем слово hello , компонент перерисуется пять раз.
Расширение Profiler
Есть более удобный способ отслеживать отрисовку компонентов. Для этого нужно установить расширения для браузера React Developer Tools . Расширение добавляет несколько инструментов для работы с React-приложением. Один из них — это Profiler, позволяющий отслеживать отрисовку компонентов.
Нужно открыть вкладку Profiler в devtools, нажать кнопку записи слева и после этого взаимодействовать с приложением — например, ввести в поле любое значение, после этого остановить запись
Так будет выглядеть результат, если ввести в поле строку «hello» :
В правой верхней части можно переключаться между шагами рендера, Profiler сохраняет информацию о каждом рендере и отображает информацию сколько времени отрисовывался каждый компонент.
Пока может показаться, что это немного. Но в сложных приложениях с большим количеством компонентов такой инструмент может быть очень полезным.
Обычно при сборке React-приложение оптимизируется и поддержка профайлера из него убирается. Но вы можете использовать флаг —profile в команде сборки, если используете create-react-app , что бы оставить поддержку инструмента на сервере:
-- --profile
Инструмент memo
В нашем приложении изменяется состояние корневого компонента. Это влечет за собой его перерисовку, что, в свою очередь, влечет за собой перерисовку всех дочерних компонентов. Компонент Greeting каждый раз перерисовывается. Это не всегда нужно, ведь в компоненте каждый раз рендерится одно и то же сообщение.
Такое поведение можно изменить, в React для этого используется инструмент мемоизации memo . Достаточно в него передать наш компонент:
import memo > from 'react'; const Memoized = memo(MyComponent);
Вот как будет выглядеть наше приложение:
import memo, useState, useEffect > from 'react'; const Greeting = memo(() => useEffect(() => console.log(`Компонент Greeting отрисован в $new Date().toLocaleTimeString()>`); >); return h3>Hello, world!h3>; >); const App = () => const [name, setName] = useState(''); return ( <> input value=name> onChange=(e) => setName(e.target.value)> /> Greeting /> > ); >;
Теперь, если ввести в поле значение, то по профайлеру или выводу логов мы увидим, что компонент Greeting не отрисовывается.
Это не значит, что компонент теперь всегда будет отрисовываться один раз. Если мы будем передавать в компонент измененный пропс, то перерисовка произойдет все равно:
import memo, useState, useEffect > from 'react'; const Greeting = memo(( name >) => useEffect(() => console.log(`Компонент Greeting отрисован в $new Date().toLocaleTimeString()>`); >); return h3>`Hello, $name>!`>h3>; >); const App = () => const [name, setName] = useState(''); return ( <> input value=name> onChange=(e) => setName(e.target.value)> /> Greeting name=name> /> > ); >;
Мемоизация и useMemo
Изменение пропсов может привести к ситуации, когда сами данные не меняются, но меняется ссылка на объект. В этом случае перерисовка произойдет, и memo не поможет. Немного модифицируем наше приложение для демонстрации такой ситуации:
import memo, useState > from 'react'; const Button = memo(( onClick >) => console.log(`Компонент Button отрисован в $new Date().toLocaleTimeString()>`); return button onClick=onClick>>Нажми меняbutton>; >); const Greeting = memo(( name >) => console.log(`Компонент Greeting отрисован в $new Date().toLocaleTimeString()>`); return h3>`Hello, $name>!`>h3>; >); const App = () => const [name, setName] = useState(''); const buttonHandler = () => setName('world'); return ( <> input value=name> onChange=(e) => setName(e.target.value)> /> Greeting name=name> /> Button onClick=buttonHandler> /> > ); >;
В приложение добавлен компонент Button , который принимает функцию для события onClick . Несмотря на то, что компонент мемоизирован, он все равно будет перерисовываться при каждой отрисовке App . Это происходит, потому что в компонент передается функция buttonHandler() . При каждой отрисовке App создается новая функция. React видит изменение ссылки на функцию и вызывает перерисовку компонента Button .
Чтобы избежать ненужных перерисовок компонента Button , нужно использовать хук useMemo . Он принимает функцию, которая возвращает какой-то результат. Хук запоминает этот результат и возвращает его каждый раз, не вызывая повторно переданную функцию.
Первым параметром в хук передается функция, возвращаемое значение которой нужно запомнить, а вторым параметром — массив зависимостей. При изменении любой из указанных зависимостей будет вызываться переданная функция и вычисляться новый результат. С этой точки зрения useMemo похож на useEffect .
Доработаем наше приложение так, чтобы в качестве результата возвращалась функция:
import memo, useState, useMemo > from 'react'; const Button = memo(( onClick >) => console.log(`Компонент Button отрисован в $new Date().toLocaleTimeString()>`); return button onClick=onClick>>Нажми меняbutton>; >); const Greeting = memo(( name >) => console.log(`Компонент Greeting отрисован в $new Date().toLocaleTimeString()>`); return h3>`Hello, $name>!`>h3>; >); const App = () => const [name, setName] = useState(''); const buttonHandler = useMemo(() => () => setName('world'), []); return ( <> input value=name> onChange=(e) => setName(e.target.value)> /> Greeting name=name> /> Button onClick=buttonHandler> /> > ); >;
В useMemo передается функция () => () => setName(‘world’) . Эта функция возвращает функцию () => setName(‘world’) . В итоге эта возвращенная функция будет мемоизирована, и при отрисовке компонента App ссылка на функцию в переменной buttonHandler будет оставаться той же. Компонент Button перерисовываться не будет.
Обычно useMemo используют для каких-то сложных вычислений, чтобы не пересчитывать результат при одних и тех же параметрах. А для сохранения ссылки на функцию, как в нашем случае, используется похожий хук useCallback .
Важно знать, что useMemo() не гарантирует, что не будет нового вычисления. В какой-то момент React может вызвать мемоизированную функцию — обычно это происходит для освобождения новых ресурсов. Это значит, что вы не должны строить логику приложения полагаясь на useMemo() , эта функция предназначена лишь для оптимизации работы компонентов.
useCallback
Хук useCallback() работает похожим образом как useMemo , только уже мемоизирует не результат вызова переданной функции, а саму функцию. Это позволяет немного сократить код и избавиться от лишних объявлений функций:
const buttonHandler = useCallback(() => setName('world'), []);
Как и useMemo , useCallback принимает вторым параметром массив зависимостей.
useCallback¶
useCallback — это хук React, который позволяет кэшировать определение функции между повторными рендерингами.
const cachedFn = useCallback(fn, dependencies);
Описание¶
useCallback(fn, dependencies) ¶
Вызовите useCallback на верхнем уровне вашего компонента, чтобы кэшировать определение функции между рендерингами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import useCallback > from 'react'; export default function ProductPage( productId, referrer, theme, >) const handleSubmit = useCallback( (orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer] ); // . >
Параметры¶
- fn : Значение функции, которое вы хотите кэшировать. Она может принимать любые аргументы и возвращать любые значения. React вернет (не вызовет!) вашу функцию обратно во время первоначального рендера. При последующих рендерах React вернет вам ту же функцию, если зависимости не изменились с момента последнего рендера. В противном случае, он отдаст вам функцию, которую вы передали во время текущего рендеринга, и сохранит ее на случай, если она может быть использована позже. React не будет вызывать вашу функцию. Функция возвращается вам, чтобы вы могли решить, когда и стоит ли ее вызывать.
- dependencies : Список всех реактивных значений, на которые ссылается код fn . Реактивные значения включают пропсы, состояние, а также все переменные и функции, объявленные непосредственно в теле вашего компонента. Если ваш линтер настроен на React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть написан inline по типу [dep1, dep2, dep3] . React будет сравнивать каждую зависимость с предыдущим значением, используя алгоритм сравнения Object.is .
Возвраты¶
При первоначальном рендере useCallback возвращает переданную вами функцию fn .
При последующих рендерах она либо вернет уже сохраненную функцию fn из последнего рендера (если зависимости не изменились), либо вернет функцию fn , которую вы передали во время этого рендера.
Ограничения¶
- useCallback — это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, создайте новый компонент и перенесите состояние в него.
- React не будет выбрасывать кэшированную функцию, если для этого нет особой причины. Например, в разработке React выбрасывает кэш, когда вы редактируете файл вашего компонента. Как в разработке, так и в продакшене, React отбрасывает кэш, если ваш компонент приостанавливается во время начального монтирования. В будущем React может добавить больше функций, которые будут использовать преимущества отбрасывания кэша — например, если React в будущем добавит встроенную поддержку виртуализированных списков, то будет иметь смысл отбрасывать кэш для элементов, которые прокручиваются из области просмотра виртуализированной таблицы. Это должно соответствовать вашим ожиданиям, если вы полагаетесь на useCallback в качестве оптимизации производительности. В противном случае, более подходящими могут быть state variable или ref.
Использование¶
Пропуск повторного рендеринга компонентов¶
При оптимизации производительности рендеринга иногда необходимо кэшировать функции, которые вы передаете в команду
Чтобы кэшировать функцию между повторными рендерингами компонента, оберните ее определение в хук useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import useCallback > from 'react'; function ProductPage( productId, referrer, theme >) const handleSubmit = useCallback( (orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer] ); // . >
Вам нужно передать две вещи в useCallback :
- Определение функции, которую вы хотите кэшировать между повторными рендерами.
- Список зависимостей, включающий каждое значение в вашем компоненте, которое используется в вашей функции.
При первом рендере возвращаемая функция, которую вы получите от useCallback , будет функцией, которую вы передали.
При последующих рендерах React будет сравнивать зависимости с зависимостями, которые вы передали во время предыдущего рендера. Если ни одна из зависимостей не изменилась (по сравнению с Object.is ), useCallback вернет ту же функцию, что и раньше. В противном случае, useCallback вернет функцию, которую вы передали на этом рендере.
Другими словами, useCallback кэширует функцию между повторными рендерами, пока ее зависимости не изменятся.
Давайте рассмотрим пример, чтобы увидеть, когда это полезно.
Скажем, вы передаете функцию handleSubmit от ProductPage компоненту ShippingForm :
1 2 3 4 5 6 7 8 9
function ProductPage( productId, referrer, theme >) // . return ( div className=theme>> ShippingForm onSubmit=handleSubmit> /> /div> ); // . >
Вы заметили, что при переключении параметра theme приложение на мгновение замирает, но если убрать из JSX, то все работает быстро. Это говорит о том, что стоит попробовать оптимизировать компонент ShippingForm .
По умолчанию, когда компонент рендерится, React рекурсивно рендерит все его дочерние компоненты. Вот почему, когда ProductPage рендерится с другой темой , компонент ShippingForm также рендерится. Это хорошо для компонентов, которые не требуют больших вычислений для повторного рендеринга. Но если вы убедились, что повторный рендеринг медленный, вы можете сказать ShippingForm пропустить повторный рендеринг, когда его пропсы такие же, как и при последнем рендере, обернув его в memo :
1 2 3 4 5 6 7
import memo > from 'react'; const ShippingForm = memo(function ShippingForm( onSubmit, >) // . >);
После этого изменения ShippingForm будет пропускать повторный рендеринг, если все его пропсы те же, что и при последнем рендеринге. Вот когда кэширование функции становится важным! Допустим, вы определили handleSubmit без useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function ProductPage( productId, referrer, theme >) // Every time the theme changes, this will be a different function. function handleSubmit(orderDetails) post('/product/' + productId + '/buy', referrer, orderDetails, >); > return ( div className=theme>> /* . so ShippingForm's props will never be the same, and it will re-render every time */> ShippingForm onSubmit=handleSubmit> /> /div> ); >
В JavaScript функция () <> или () => <> всегда создает разную функцию, подобно тому, как объектный литерал <> всегда создает новый объект. Обычно это не является проблемой, но это означает, что пропс ShippingForm никогда не будет одинаковым, и ваша оптимизация memo не будет работать. Вот здесь-то и пригодится useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
function ProductPage( productId, referrer, theme >) // Tell React to cache your function between re-renders. const handleSubmit = useCallback( (orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer] ); // . so as long as these dependencies don't change. return ( div className=theme>> /* . ShippingForm will receive the same props and can skip re-rendering */> ShippingForm onSubmit=handleSubmit> /> /div> ); >
Вернув handleSubmit в useCallback , вы гарантируете, что это будет одна и та же функция между повторными рендерами (пока не изменятся зависимости). Вы не обязаны обертывать функцию в useCallback , если только вы не делаете это по какой-то конкретной причине. В данном примере причина в том, что вы передаете ее компоненту, обернутому в memo , и это позволяет ему пропустить повторный рендеринг. Есть и другие причины, по которым вам может понадобиться useCallback , которые описаны далее на этой странице.
Вы должны полагаться на useCallback только в качестве оптимизации производительности. Если ваш код не работает без него, найдите основную проблему и сначала устраните ее. Затем вы можете добавить useCallback обратно.
Как useCallback связан с useMemo?
Вы часто будете видеть useMemo вместе с useCallback . Они оба полезны, когда вы пытаетесь оптимизировать дочерний компонент. Они позволяют вам memoize (или, другими словами, кэшировать) то, что вы передаете вниз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import useMemo, useCallback > from 'react'; function ProductPage( productId, referrer >) const product = useData('/product/' + productId); const requirements = useMemo(() => // Calls your function and caches its result return computeRequirements(product); >, [product]); const handleSubmit = useCallback( (orderDetails) => // Caches your function itself post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer] ); return ( div className=theme>> ShippingForm requirements=requirements> onSubmit=handleSubmit> /> /div> ); >
Разница в том, что именно они позволяют вам кэшировать:
- useMemo кэширует результат вызова вашей функции. В этом примере он кэширует результат вызова computeRequirements(product) , чтобы он не изменился, если product не изменился. Это позволяет передавать объект requirements вниз без ненужного повторного рендеринга ShippingForm . При необходимости React будет вызывать переданную вами функцию во время рендеринга для вычисления результата.
- useCallback кэширует саму функцию. В отличие от useMemo , он не вызывает предоставленную вами функцию. Вместо этого она кэширует предоставленную вами функцию, так что handleSubmit сама по себе не изменяется, если только productId или referrer не изменились. Это позволяет вам передавать функцию handleSubmit вниз без ненужного повторного рендеринга ShippingForm . Ваш код не будет выполняться до тех пор, пока пользователь не отправит форму.
Если вы уже знакомы с useMemo , вам будет полезно представить useCallback следующим образом:
1 2 3 4
// Simplified implementation (inside React) function useCallback(fn, dependencies) return useMemo(() => fn, dependencies); >
Должны ли вы везде добавлять useCallback?
Если ваше приложение похоже на этот сайт, и большинство взаимодействий являются грубыми (например, замена страницы или целого раздела), мемоизация обычно не нужна. С другой стороны, если ваше приложение больше похоже на редактор рисунков, и большинство взаимодействий являются гранулированными (например, перемещение фигур), то мемоизация может оказаться очень полезной.
Кэширование функции с useCallback полезно только в нескольких случаях:
- Вы передаете ее в качестве параметра компоненту, обернутому в memo . Вы хотите пропустить повторный рендеринг, если значение не изменилось. Мемоизация позволяет вашему компоненту повторно отображаться только в том случае, если изменились зависимости.
- Функция, которую вы передаете, позже будет использоваться как зависимость какого-то Hook. Например, другая функция, обернутая в useCallback , зависит от нее, или вы зависите от этой функции из useEffect .
В других случаях нет никакой пользы от обертывания функции в useCallback . Вреда от этого тоже нет, поэтому некоторые команды предпочитают не думать об отдельных случаях и мемоизировать как можно больше. Недостатком является то, что код становится менее читабельным. Кроме того, не вся мемоизация эффективна: одного значения, которое «всегда новое», достаточно, чтобы сломать мемоизацию для всего компонента.
Обратите внимание, что useCallback не предотвращает создание функции. Вы всегда создаете функцию (и это fine!), но React игнорирует это и возвращает вам кэшированную функцию, если ничего не изменилось.
На практике вы можете сделать ненужной мемоизацию, следуя нескольким принципам:.
- Когда компонент визуально оборачивает другие компоненты, пусть он принимает JSX в качестве дочерних компонентов Тогда, если компонент-обертка обновляет свое состояние, React знает, что его дочерние компоненты не нужно перерисовывать.
- Предпочитайте локальное состояние и не поднимайте состояние вверх дальше, чем это необходимо. Не храните переходные состояния, такие как формы и то, наведен ли элемент на вершину вашего дерева или в глобальной библиотеке состояний.
- Сохраняйте чистоту логики рендеринга Если повторный рендеринг компонента вызывает проблему или приводит к заметным визуальным артефактам, это ошибка в вашем компоненте! Исправьте ошибку вместо того, чтобы добавлять мемоизацию.
- Избегайте ненужных Эффектов, обновляющих состояние Большинство проблем с производительностью в приложениях React вызваны цепочками обновлений, исходящих от Эффектов, которые заставляют ваши компоненты рендериться снова и снова.
- Попробуйте удалить ненужные зависимости из ваших Эффектов. Например, вместо мемоизации часто проще переместить какой-то объект или функцию внутрь Эффекта или за пределы компонента.
Если конкретное взаимодействие все еще кажется нестабильным, используйте профилировщик React Developer Tools, чтобы увидеть, какие компоненты больше всего выигрывают от мемоизации, и добавьте мемоизацию там, где это необходимо. Эти принципы облегчают отладку и понимание ваших компонентов, поэтому следовать им полезно в любом случае. В перспективе мы исследуем возможность автоматической мемоизации, чтобы решить эту проблему раз и навсегда.
Разница между useCallback и объявлением функции напрямую¶
1. Пропуск повторного рендеринга с useCallback и memo ¶
В этом примере компонент ShippingForm искусственно замедлен, чтобы вы могли увидеть, что происходит, когда React-компонент, который вы рендерите, действительно медленный. Попробуйте увеличить счетчик и переключить тему.
Увеличение счетчика кажется медленным, потому что это заставляет замедленный ShippingForm перерисовываться. Это ожидаемо, потому что счетчик изменился, и вам нужно отразить новый выбор пользователя на экране.
Далее попробуйте переключить тему. Благодаря useCallback вместе с memo , это происходит быстро, несмотря на искусственное замедление! ShippingForm пропускает повторное отображение, потому что функция handleSubmit не изменилась. Функция handleSubmit не изменилась, потому что productId и referrer (ваши зависимости useCallback ) не изменились с момента последнего рендеринга.
App.js ProductPage.js ShippingForm.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import useState > from 'react'; import ProductPage from './ProductPage.js'; export default function App() const [isDark, setIsDark] = useState(false); return ( <> label> input type="checkbox" checked=isDark> onChange=<(e) => setIsDark(e.target.checked) > /> Dark mode /label> hr /> ProductPage referrerId="wizard_of_oz" productId=123> theme=isDark ? 'dark' : 'light'> /> /> ); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import useCallback > from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage( productId, referrer, theme, >) const handleSubmit = useCallback( (orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer] ); return ( div className=theme>> ShippingForm onSubmit=handleSubmit> /> /div> ); > function post(url, data) // Imagine this sends a request. console.log('POST /' + url); console.log(data); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
import memo, useState > from 'react'; const ShippingForm = memo(function ShippingForm( onSubmit, >) const [count, setCount] = useState(1); console.log( '[ARTIFICIALLY SLOW] Rendering ' ); let startTime = performance.now(); while (performance.now() - startTime 500) // Do nothing for 500 ms to emulate extremely slow code > function handleSubmit(e) e.preventDefault(); const formData = new FormData(e.target); const orderDetails = . Object.fromEntries(formData), count, >; onSubmit(orderDetails); > return ( form onSubmit=handleSubmit>> p> b> Note: code>ShippingForm/code> is artificially slowed down! /b> /p> label> Number of items: button type="button" onClick= => setCount(count - 1)> > – /button> count> button type="button" onClick= => setCount(count + 1)> > + /button> /label> label> Street: input name="street" /> /label> label> City: input name="city" /> /label> label> Postal code: input name="zipCode" /> /label> button type="submit">Submit/button> /form> ); >); export default ShippingForm;
2. Всегда перерендеринг компонента¶
В этом примере реализация ShippingForm также искусственно замедлена, чтобы вы могли увидеть, что происходит, когда какой-либо компонент React, который вы рендерите, действительно медленный. Попробуйте увеличить счетчик и переключить тему.
В отличие от предыдущего примера, переключение темы теперь также происходит медленно! Это происходит потому, что в этой версии нет вызова useCallback , поэтому handleSubmit всегда является новой функцией, и замедленный компонент ShippingForm не может пропустить повторный рендеринг.
App.js ProductPage.js ShippingForm.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import useState > from 'react'; import ProductPage from './ProductPage.js'; export default function App() const [isDark, setIsDark] = useState(false); return ( <> label> input type="checkbox" checked=isDark> onChange=<(e) => setIsDark(e.target.checked) > /> Dark mode /label> hr /> ProductPage referrerId="wizard_of_oz" productId=123> theme=isDark ? 'dark' : 'light'> /> /> ); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import ShippingForm from './ShippingForm.js'; export default function ProductPage( productId, referrer, theme, >) function handleSubmit(orderDetails) post('/product/' + productId + '/buy', referrer, orderDetails, >); > return ( div className=theme>> ShippingForm onSubmit=handleSubmit> /> /div> ); > function post(url, data) // Imagine this sends a request. console.log('POST /' + url); console.log(data); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
import memo, useState > from 'react'; const ShippingForm = memo(function ShippingForm( onSubmit, >) const [count, setCount] = useState(1); console.log( '[ARTIFICIALLY SLOW] Rendering ' ); let startTime = performance.now(); while (performance.now() - startTime 500) // Do nothing for 500 ms to emulate extremely slow code > function handleSubmit(e) e.preventDefault(); const formData = new FormData(e.target); const orderDetails = . Object.fromEntries(formData), count, >; onSubmit(orderDetails); > return ( form onSubmit=handleSubmit>> p> b> Note: code>ShippingForm/code> is artificially slowed down! /b> /p> label> Number of items: button type="button" onClick= => setCount(count - 1)> > – /button> count> button type="button" onClick= => setCount(count + 1)> > + /button> /label> label> Street: input name="street" /> /label> label> City: input name="city" /> /label> label> Postal code: input name="zipCode" /> /label> button type="submit">Submit/button> /form> ); >); export default ShippingForm;
Однако, вот тот же код с искусственным замедлением. Отсутствие useCallback ощущается заметно или нет?
App.js ProductPage.js ShippingForm.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import useState > from 'react'; import ProductPage from './ProductPage.js'; export default function App() const [isDark, setIsDark] = useState(false); return ( <> label> input type="checkbox" checked=isDark> onChange=<(e) => setIsDark(e.target.checked) > /> Dark mode /label> hr /> ProductPage referrerId="wizard_of_oz" productId=123> theme=isDark ? 'dark' : 'light'> /> /> ); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import ShippingForm from './ShippingForm.js'; export default function ProductPage( productId, referrer, theme, >) function handleSubmit(orderDetails) post('/product/' + productId + '/buy', referrer, orderDetails, >); > return ( div className=theme>> ShippingForm onSubmit=handleSubmit> /> /div> ); > function post(url, data) // Imagine this sends a request. console.log('POST /' + url); console.log(data); >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
import memo, useState > from 'react'; const ShippingForm = memo(function ShippingForm( onSubmit, >) const [count, setCount] = useState(1); console.log('Rendering '); function handleSubmit(e) e.preventDefault(); const formData = new FormData(e.target); const orderDetails = . Object.fromEntries(formData), count, >; onSubmit(orderDetails); > return ( form onSubmit=handleSubmit>> label> Number of items: button type="button" onClick= => setCount(count - 1)> > – /button> count> button type="button" onClick= => setCount(count + 1)> > + /button> /label> label> Street: input name="street" /> /label> label> City: input name="city" /> /label> label> Postal code: input name="zipCode" /> /label> button type="submit">Submit/button> /form> ); >); export default ShippingForm;
Довольно часто код без мемоизации работает нормально. Если ваши взаимодействия достаточно быстрые, мемоизация не нужна.
Помните, что вам нужно запустить React в производственном режиме, отключить React Developer Tools и использовать устройства, похожие на те, которые есть у пользователей вашего приложения, чтобы получить реальное представление о том, что на самом деле замедляет работу вашего приложения.
Обновление состояния из мемоизированного обратного вызова¶
Иногда вам может потребоваться обновить состояние на основе предыдущего состояния из мемоизированного обратного вызова.
Эта функция handleAddTodo указывает todos как зависимость, потому что она вычисляет следующий todos из него:
1 2 3 4 5 6 7 8 9 10 11 12
function TodoList() const [todos, setTodos] = useState([]); const handleAddTodo = useCallback( (text) => const newTodo = id: nextId++, text >; setTodos([. todos, newTodo]); >, [todos] ); // . >
Обычно вы хотите, чтобы мемоизированные функции имели как можно меньше зависимостей. Когда вы читаете некоторое состояние только для вычисления следующего состояния, вы можете устранить эту зависимость, передав вместо него функцию updater:
1 2 3 4 5 6 7 8 9
function TodoList() const [todos, setTodos] = useState([]); const handleAddTodo = useCallback((text) => const newTodo = id: nextId++, text >; setTodos((todos) => [. todos, newTodo]); >, []); // ✅ No need for the todos dependency // . >
Здесь вместо того, чтобы сделать todos зависимостью и читать ее внутри, вы передаете в React инструкцию о том, как обновить состояние ( todos => [. todos, newTodo] ). Подробнее о функциях обновления
Предотвращение слишком частого срабатывания эффекта¶
Иногда вы можете захотеть вызвать функцию внутри Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
function ChatRoom( roomId >) const [message, setMessage] = useState(''); function createOptions() return serverUrl: 'https://localhost:1234', roomId: roomId, >; > useEffect(() => const options = createOptions(); const connection = createConnection(); connection.connect(); // . >); >
Это создает проблему. Каждое реактивное значение должно быть объявлено зависимостью вашего Эффекта Однако, если вы объявите createOptions как зависимость, это заставит ваш Эффект постоянно переподключаться к чату:
1 2 3 4 5 6 7
useEffect(() => const options = createOptions(); const connection = createConnection(); connection.connect(); return () => connection.disconnect(); >, [createOptions]); // Problem: This dependency changes on every render // .
Чтобы решить эту проблему, вы можете обернуть функцию, которую нужно вызвать из Effect, в useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
function ChatRoom( roomId >) const [message, setMessage] = useState(''); const createOptions = useCallback(() => return serverUrl: 'https://localhost:1234', roomId: roomId, >; >, [roomId]); // ✅ Only changes when roomId changes useEffect(() => const options = createOptions(); const connection = createConnection(); connection.connect(); return () => connection.disconnect(); >, [createOptions]); // ✅ Only changes when createOptions changes // . >
Это гарантирует, что функция createOptions будет одинаковой между повторными рендерингами, если roomId одинаков. Однако, еще лучше устранить необходимость в зависимости от функции. Переместите вашу функцию внутрь Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
function ChatRoom( roomId >) const [message, setMessage] = useState(''); useEffect(() => function createOptions() // ✅ No need for useCallback or function dependencies! return serverUrl: 'https://localhost:1234', roomId: roomId, >; > const options = createOptions(); const connection = createConnection(); connection.connect(); return () => connection.disconnect(); >, [roomId]); // ✅ Only changes when roomId changes // . >
Теперь ваш код стал проще и не нуждается в useCallback . Подробнее об удалении зависимостей от эффектов.
Оптимизация пользовательского хука¶
Если вы пишете пользовательский хук, рекомендуется обернуть все функции, которые он возвращает, в useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
function useRouter() const dispatch > = useContext(RouterStateContext); const navigate = useCallback( (url) => dispatch( type: 'navigate', url >); >, [dispatch] ); const goBack = useCallback(() => dispatch( type: 'back' >); >, [dispatch]); return navigate, goBack, >; >
Это гарантирует, что потребители вашего Hook смогут при необходимости оптимизировать свой собственный код.
Устранение неполадок¶
Каждый раз, когда мой компонент рендерится, useCallback возвращает другую функцию¶
Убедитесь, что вы указали массив зависимостей в качестве второго аргумента!
Если вы забудете про массив зависимостей, useCallback будет возвращать каждый раз новую функцию:
1 2 3 4 5 6 7 8 9
function ProductPage( productId, referrer >) const handleSubmit = useCallback((orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >); // Returns a new function every time: no dependency array // . >
Это исправленная версия, передающая массив зависимостей в качестве второго аргумента:
1 2 3 4 5 6 7 8
function ProductPage( productId, referrer >) const handleSubmit = useCallback((orderDetails) => post('/product/' + productId + '/buy', referrer, orderDetails, >); >, [productId, referrer]); // ✅ Does not return a new function unnecessarily // .
Если это не помогло, то проблема в том, что по крайней мере одна из ваших зависимостей отличается от предыдущего рендера. Вы можете отладить эту проблему, вручную записав логи зависимостей в консоль:
1 2 3 4 5 6 7 8
const handleSubmit = useCallback( (orderDetails) => // .. >, [productId, referrer] ); console.log([productId, referrer]);
Затем вы можете щелкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать «Store as a global variable» для обоих. Предположив, что первый массив был сохранен как temp1 , а второй — как temp2 , вы можете использовать консоль браузера, чтобы проверить, является ли каждая зависимость в обоих массивах одинаковой:
1 2 3
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays? Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays? Object.is(temp1[2], temp2[2]); // . and so on for every dependency .
Когда вы обнаружите, какая зависимость нарушает мемоизацию, либо найдите способ удалить ее, либо мемоизируйте и ее.
Мне нужно вызвать useCallback для каждого элемента списка в цикле, но это не разрешено¶
Предположим, что компонент Chart обернут в memo . Вы хотите пропустить повторное отображение каждого Chart в списке при повторном отображении компонента ReportList . Однако вы не можете вызвать useCallback в цикле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
function ReportList( items >) return ( article> items.map((item) => // You can't call useCallback in a loop like this: const handleClick = useCallback(() => sendReport(item); >, [item]); return ( figure key=item.id>> Chart onClick=handleClick> /> /figure> ); >)> /article> ); >
Вместо этого извлеките компонент для отдельного элемента и поместите туда useCallback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
function ReportList( items >) return ( article> items.map((item) => ( Report key=item.id> item=item> /> ))> /article> ); > function Report( item >) // ✅ Call useCallback at the top level: const handleClick = useCallback(() => sendReport(item); >, [item]); return ( figure> Chart onClick=handleClick> /> /figure> ); >
В качестве альтернативы можно убрать useCallback в последнем фрагменте и вместо этого обернуть сам Report в memo . Если параметр item не меняется, Report пропускает повторный рендеринг, поэтому Chart тоже пропускает повторный рендеринг:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
function ReportList( items >) // . > const Report = memo(function Report( item >) function handleClick() sendReport(item); > return ( figure> Chart onClick=handleClick> /> /figure> ); >);
Ссылки¶
Учим useCallback на примерах — React Hooks
Доброго времени суток, друзья. И вновь поговорим про React Hooks. Сегодня мы рассмотрим хук useCallback. В чем его польза? Какие особенности? Давайте обо всем по порядку.
Видео на эту тему.
Хук useCallback для мемоизации?
Хук useCallback похож на хук useMemo (больше информации по данному хуку вы можете получить тут) с тем отличием, что он возвращает мемоизированный колбэк, который будет обновлен, только если одна из зависимостей будет изменена.
const memoized = useCallback( () => < foo(a, b); >, [a, b], );
В примере выше можно увидеть, как происходит процесс мемоизации функции. Достаточно в первый параметр передать функцию, которую требуется мемоизировать, поместив ее в колбек и передав ей зависимые параметры. Вторым параметром useCallback является массив зависимых параметров, благодаря которому будет происходить мемоизация данного колбэка. Если один из переданных параметров изменится, то useCallback вернет новую мемоизированную функцию, при этом, если зависимые параметры не будут изменяться в функциональном компоненте React, то будет использоваться копия данной функции, что позволит оптимизировать применение памяти реактом.
Хук useCallback на практике
Для демонстрации различий в оптимизации кода с помощью хука useCallback и без него, создадим компонент, в который будем передавать функции и поместим вызов этих функций в useEffect для контроля изменений при ее вызове. Это будет означать, что если функция не была изменена (мемоизирована), то ее вызов не будет происходить в этом компоненте.
import React, < useEffect >from "react"; export default function Child(< updateOne, updateTwo >) < useEffect(() =>< updateOne(); >, [updateOne]); useEffect(() => < updateTwo(); >, [updateTwo]); return >
Поместим данный компонент в App.js, в котором будет храниться состояние двух счетчиков (использую хук useState), также там будут находиться две кнопки, при клике на которые вызываются функции для изменения значения счетчиков.
Самая интересная часть, это две функции, находящиеся в App.js, которые передаются дочернему компоненту Child. Функции будут просто вызывать соnsole.log и их единственное различие будет заключаться в том, что одна из них будет обернута хуком useCallback и иметь в зависимости состояние одного из счетчиков.
import React, < useEffect, useState, useCallback >from "react"; import Child from "./child"; import "./styles.css"; export default function App() < const [counter, setCounter] = useState(0); const [counterTwo, setCounterTwo] = useState(0); const updateOne = () =>< console.log( "Я не мемоизирован" ); >; const updateTwo = useCallback(() => < console.log( "Я мемоизирован!" ); >, [counter]); return (
updateTwo = /> ); >
Если кликнуть на кнопку “One”, то можно увидеть вызов одновременно двух консолей, при этом, если кликнуть на кнопку “Two”, то будет виден вызов только одной консоли с текстом «Я не мемоизирован». Почему так происходит? Давайте рассмотрим этот процесс подробнее.
По клику на кнопку “One” происходит вызов функции setCounter, в которой мы меняем значение counter c прибавлением к нему единицы. Функция updateTwo мемоизирована и ее новая копия появляется только в момент, когда изменяется значение состояния counter. Как следствие мы получаем нашу обновленную функцию и далее по ссылке в компоненте Child происходит проверка на равенство в useEffect. Ссылка на старую функцию изменилась, что приводит к принудительному перерендереванию дочернего компонента с вызовом самой функции.
При нажатии на кнопку “Two” также происходит изменение счетчика counterTwo на одну единицу. Но данное состояние не указано в массиве зависимостей хука useCallback. Это означает, что функция updateTwo не обновится, а React будет использовать ее закэшированную версию. При дальнейшем сравнении в компоненте Child ссылка на функцию останется прежней, что не приведет к ее вызову.
Заключение
Сегодня мы разобрали на примере счетчик с родительским компонентом и как можно мемоизироивать функции, используя хук useCallback. Использование разработчиками хуков useMemo и useCallback позволяет в значительной степени оптимизировать приложения. Надеюсь, что данный материал был вам полезен. Учитесь, думайте, пишите код. Удачного кодинга, друзья!
Подписывайтесь на наш канал в Telegram и на YouTube для получения самой последней и актуальной информации.