Оптимизация производительности функциональных компонентов React
Разработчики React делают все, чтобы React работал практически молниеносно, Virtual DOM работает на столько быстро, на сколько это возможно. Но случается, что даже небольшие приложения по прежнему работают очень медленно. Все же React не делает чудес и все также многое зависит от разработчика приложения. От него требуется понимание работы React и принципы эффективного использования в том или ином случае.
В React существует уже множество инструментов для оптимизации, такие как React.lazy , PureComponent и т.д. Мы рассмотрим только моменты связанные с оптимизацией применимой функциональным компонентам.
Базовые подходы оптимизации в React
- Сокращение количества отрисовок.
- Уменьшение количества повторных вычислений. Сокращение вычислений для функциональных компонентов, когда рендер будет перезапускать вызов функции с самого начала.
При оптимизации классовых компонентов в основном используетя PureComponent либо метод жизненного цикла shouldComponentUpdate . В функциональных компонентах нет ни периода объявления, ни класса, так как же оптимизировать производительность?
Сокращение количества ререндеров
Компоненты имеют состояние, и когда под действиями пользователя эти состояния изменятется, то требуется перерисовка компонента. Компонент React может перерисовыватьстя сколь угодно раз. Но очень часто в этом нет необходимости и можно обойтись без перерисовки, так как это очень влияет на производительность приложения.
По каким причинам происходят повторные рендеринги:
- Изменилось состояние компонента
- Изменились props компонента
В React, после повторного рендеринга родителя также происходит рендеринг дочерних компонентов, что сильно влияет на производительность.
useEffect
Как известно useEffect включает в себя аналоги классических хуков componentDidMount , componentDidUpdate и componentWillUnmount . Нас интересуют только первые два, которые влияют на количество рендеров.
Для того, чтобы небыло лишних отрисовок нужно понимать в каком случае срабатывает componentDidMount и в каком и как — componentDidUpdate . Стоит проконтролировать зависимости useEffect , которые находятся во втором параметре, если есть таковые.
В случае если параметр отсутствует, useEffect будет срабатывать при каждом рендеринге. Бывают случаи, когда этот параметр просто забывается. Мы таже можем указать конкретную зависимость и будет срабатывать только после изменения ее значения. Все это — аналогия с componentDidUpdate .
export const ExampleComponent = () => const [count, setCount] = useState(0); useEffect(() => console.log(count); >) const handleButtonClick = () => setCount(count++); return (<>button onClick=handleButtonClick>>Кнопкаbutton><>); >
В этом случае useEffect будет реагировать на каждое изменение count .
Если же на место второго параметра выставить пустой массив, то будет считаться, что у useEffect нет зависимостей и он выполнится разово. Это аналог componentDidMount .
export const ExampleComponent = () => const [count, setCount] = useState(0); useEffect(() => setCount(count++); >, []) return (<>span>count: count>span>>); > // count: 1
useCallback
useCallback возвращает мемоизированную версию функции. Данный хук запоминает версию обратного вызова, для избежании повторных рендерингов дочерних элементов.
useCallbak похож на useMemo , но похожесть обманчива. useCallback ближе к useRef , чем к мемоизации. useCallback возвращает мемоизированный колбек без его вызова, useMemo — мемоизированное значение после вызова функции.
Возьмем такой пример:
export const FirstComponent = () => const [count, setCount] = useState(false); return ( [. ] SecondComponent onClick= => showData(count); >/> [. ] ); >
Этот компонент будет вызывать повторную визуализацию SecondComponent элемента каждый раз, когда это делает FirstComponent , даже если SecondComponent элемент является PureComponent или завернут в React.memo , потому что при каждом рендеринге onClick будет отличаться. useCallback может справиться с этой ситуацией так:
export const FirstComponent = () => const [count, setCount] = useState(false); const handleClick = useCallback( () => showData(count); >, [count], ); return ( [. ] SecondComponent onClick= => handleClick>/> [. ] ); >
Теперь handleClick будет иметь то же значение, пока count не изменится, что позволит сократить количество раз, когда SecondComponent рендерится.
Мемоизация
React.memo()
При принятии решения об обновлении DOM, React сначала отрисовывает ваш компонент, а затем сравнивает результат с предыдущим результатом отрисовки. Если результаты рендеринга отличаются, React обновляет DOM. Сравнение текущего и предыдущего результатов рендеринга происходит быстро. Но при некоторых обстоятельствах вы можете ускорить процесс.
Когда компонент обернут в React.memo() , React выводит компонент и запоминает результат. Перед следующим рендерингом производится неглубокое сравнение, если новый props тот же самый, React повторно использует запомненный результат, пропуская следующий рендеринг.
Давайте посмотрим, что за воспоминания в действии. Функциональный компонент Movie обернут в React.memo() :
export const ExampleComponent = ( count >) => return ( <> span>Счетчик: count>span> > ); > export const MemoizedExampleComponent = React.memo(ExampleComponent);
Вы получаете прирост производительности: повторно используя записанный контент, React пропускает рендеринг компонента и не выполняет виртуальную проверку разницы DOM.
По факту, React.memo это полный аналог PureComponent или shouldComponentUpdate , только в функциональном компоненте.
При использовании React.memo React производит shallow-сравнение. Можно изменить стандартное поведение и задать вторым параметром функцию comparer
export const exampleComponentPropsAreEqual(prev, next) return prev.count === next.count; > const MemoizedExampleComponent = React.memo(ExampleComponent, exampleComponentPropsAreEqual);
useMemo
useMemo — запоминает результат выполнения функции между рендерами, избегает повторных дорогостоящих вычислений, при неизменных переменных.
Первым параметром задается функция которая будет мемоизироваться, вторым — массив зависимостей.
В случае изменения одной зависимостей функция будет пересчитана.
const exampleList = useMemo( () => items.map( item => ( . item, date: convertToLocalDateFunc(item.date) >), ), [items], );
Дополнение
Не использовать анонимные фунцкции и литералы объектов в шаблонах
Обеъкты не имеют постоянного места хранения, но отнимают производительность, так как при каждой перерисовке приходится выделять новое место в памяти.
// так не делать export const FirstExample = () => return ( button onClick=() => console.log('Так не делать')>> Кнопка button> ); > // так уже лучше export const SecondExample = () => handleClick = () => console.log('Так уже лучше'); > return ( button onClick=this.handleClick>> Кнопка button> ); >
Тот же принцип и для анонимных функций, JavaScript так же выделяет новую памятьдля при повторном вызове компонента, вместо выделения одной ячейки памяти при использовани “именованных” функций. Так же, в этом случае, для оптимизации, следует использовать useCallback .
Избегать лишнего перемонтирования элементов в DOM
Как пример, отображение какого-либо элемента по определенному условию.
export const Dropdown = () => const [isOpen, setIsOpen] = useState(false); const toggleDropdown = () => setIsOpen(!isOpen); return ( a onClick=this.toggleDropdown>> Показать isOpen ? AnyViewComponent> : null> a>) >
Поскольку компонент удален из DOM это может вызвать браузером перерисовку. Этот момент может быть особенно дорогостоющим, особенно если это приводит к сдвигу элементов HTML.
Для того, чтобы смягчить последствия, рекомендуется избегать полного демонтажа компонентов. Вместо этого можно использовать средства CSS, такие как display: none , visibilty: hidden или opacity: 0 .
Заключение
Теперь есть все, чтобы оптимизировать жадных на производительность компонентов. Можно использовать memo , useMemo , useCallback , чтобы избежать дорогостоящих ререндеров. Но все эти инструменты включают стоимость своих собственных вычислений. К примеру, memo берет некоторые ресурсы для сравнения полей, и для хука тоже нужны дополнительные вычисления для сравнения на каждом ререндере. Инструменты оптимизации нужно использовать с умом и при необходимости, в противном случае можно добавить дополнительную задержку при выполнении. Так что излишней озабоченностью оптимизацией можно навредить. Лучше первый вариант функционала строить без оптимизации и добавлять ее следующей итерацией, при необходимости.
Были рассмотрены только основные подходы оптимизации применимые для функциональных или stateless-комопнентов. Помимо инструментов оптимизации React не стоит забывать про общие, не менее эффективные в том или ином случае, принципы оптимизации, такие как Lazy Loading , SSR , ServiceWorker , webpack оптимизации и многое другое, что выходит за границы темы данной статьи.
Повторный рендеринг и мемоизация в React
В этой статье мы привели краткие советы по повышению производительности React-приложения за счет понимания принципов повторного рендеринга и мемоизации. Чтобы извлечь из этих рекомендаций практическую пользу, важно знать, когда React ререндерит компоненты.
Как правило, повторный рендеринг происходит из-за изменения пропсов или состояния компонента. Когда компонент ререндерится, все его дочерние компоненты также подвергаются ререндерингу, если они не мемоизированы (тут есть исключения, но в большинстве случаев это так).
useRef
Частой ошибкой разработчиков React является использование useState для каждого изменяемого значения, которое нужно сохранить между процессами рендера. useState — хорошее решение, если выводимый результат зависит от значения, в противном случае лучше применять useRef .
Взгляните на следующий пример:
const [firstName, setFirstName] = useState();
return (
);
В данном примере состояние firstName обновляется каждый раз, когда пользователь набирает текст. Когда состояние обновляется, запускается повторный рендеринг, то есть каждый раз, когда пользователь набирает текст, происходит ререндеринг.
Поскольку firstName не используется в выводимых данных, мы можем заменить его на useRef и предотвратить повторный рендеринг:
const firstName = useRef();
return (
);
memo
Одной из наиболее важных концепций, которую необходимо понять для оптимизации React, является мемоизация. Мемоизация — это процесс кэширования результатов работы функции и возврата кэша при последующих запросах.
Повторный рендеринг компонента — это просто повторный вызов его функции. Если у него есть дочерние компоненты, ререндеринг будет приводить к вызову функций этих компонентов, и так далее по всему дереву. Затем результаты сравниваются с DOM, чтобы определить, нужно ли обновлять пользовательский интерфейс. Этот процесс сравнения называется согласованием.
Поскольку компоненты — это всего лишь функции, их можно мемоизировать с помощью React.memo() . Это предотвратит повторный рендеринг компонента, если не изменились зависимости (пропсы). Если у вас есть особенно тяжелый компонент, то лучше его мемоизировать, но не стоит делать так с каждым. Мемоизация задействует память и в некоторых случаях может снижать производительность.
Когда компонент мемоизируется, вместо повторного рендеринга React сравнивает новые пропсы компонента с его предыдущими пропсами. В этом случае необходимо найти оптимальное решение в зависимости от интенсивности процессов сравнения пропсов и выполнения функции. Если в пропсах содержится большой объект, то мемоизация компонента может плохо повлиять на производительность.
const HeavyComponent: FC = () => < return >
export const Heavy = React.memo(HeavyComponent);
useCallback
Важным инструментом, предотвращающим ненужный повторный рендеринг мемоизированных компонентов, является useCallback . При передаче функции в мемоизированный компонент вы можете нечаянно устранить эффект мемоизации, не мемоизируя эту функцию с помощью useCallback . Причина — в равенстве ссылок.
Как упоминалось ранее, при каждом повторном рендеринге вызывается функция компонента. Это означает, что если мы объявляем функцию в компоненте, то при каждом повторном рендеринге создается новая функция. Когда мы передаем эту функцию в качестве пропса другому компоненту, происходит изменение ссылки, даже если содержимое функции фактически не меняется. Это заставляет дочерний компонент ререндериться, даже если он мемоизирован.
export const ParentComponent = () => const handleSomething = () => <>;
return />
>;
В этом примере каждый раз, когда ParentComponent ререндерится, HeavyComponent тоже ререндерится, несмотря на то, что он мемоизирован. Мы можем исправить это, используя useCallback и предотвращая изменение ссылки.
export const ParentComponent = () => const handleSomething = useCallback(() => <>, []);
return />
>;
useMemo
Мы уже знаем, что каждый ререндеринг означает повторный вызов функции компонента. Таким образом, если функция компонента включает вызов дорогой функции, то эта дорогая функция будет вызываться при каждом повторном рендеринге. Чтобы этого избежать, можно ее мемоизировать. Первый рендеринг вызовет функцию, а последующие будут возвращать ее кэшированные результаты вместо того, чтобы запускать ее снова.
Хук useMemo намного упрощает реализацию мемоизации:
const value = useMemo(() => expensiveFunction(aDep), [aDep]);
В данном примере значение будет кэшироваться и обновляться только при изменении aDep .
Ленивая инициализация useState
Малоизвестной особенностью useState является возможность ленивой инициализации начального состояния. Если вы передадите функцию useState , она будет вызываться только при первоначальном рендеринге компонента. Это предотвращает установку начального значения при каждом повторном рендеринге, что бывает полезно, если начальное состояние требует больших вычислительных затрат. В противном случае ленивая инициализация не рекомендуется.
const initialState = () => calculateSomethingExpensive(props);
const [count, setCount] = useState(initialState);
- Geist UI: Утонченная эстетика UI в React
- Мой опыт спустя год работы с React
- Разбираемся с Render Props и HOC в React
React: полное руководство по повторному рендерингу
Представляю вашему вниманию перевод этой замечательной статьи, посвященной повторному рендерингу (re-render, далее — ререндеринг) в React.
Что такое ререндеринг?
Существует 2 основные стадии, которым следует уделять пристальное внимание, когда речь заходит о производительности в React :
- первоначальный рендеринг (initial rendering) — происходит, когда компонент впервые появляется на экране;
- ререндеринг — второй и последующие рендеринги компонента.
Ререндеринг происходит, когда React необходимо обновить приложение некоторыми данными. Обычно, это является результатом действий пользователя, получения ответа на асинхронный запрос или публикацию при подписке (паттерн «pub/sub» — публикация/подписка или издатель/подписчик) на определенные данные.
Что такое необходимый и лишний (ненужный) ререндеринги?
Необходимый (necessary) ререндеринг — это повторный рендеринг компонента, подвергшегося некоторым изменениям или получившего новые данные. Например, если пользователь вводит данные в поле (инпут), компонент, управляющий состоянием, должен обновляться при вводе каждого символа, т. е. ререндериться.
Лишний (unnecessary) ререндеринг — повторный рендеринг компонента, вызываемый различными механизмами ререндеринга в результате ошибки или неэффективной архитектуры приложения. Например, если при вводе данных в инпут пользователем ререндерится вся страница, такой ререндеринг, скорее всего, является лишним.
Сам по себе лишний рендеринг не является проблемой: React является достаточно быстрым, чтобы выполнять его незаметно для пользователя.
Однако, если ререндеринг происходит очень часто или речь идет о тяжелом с точки зрения производительности компоненте, пользовательский опыт может быть испорчен «лаганием» (временной утратой интерактивности страницей), заметными задержками в ответ на взаимодействие пользователя со страницей или полным зависанием страницы.
Когда происходит ререндеринг?
Существует 4 причины, по которым компонент подвергается ререндерингу: изменение состояния, ререндеринг родительского компонента, изменение контекста и изменение хука. Существует распространенный миф о том, что ререндеринг происходит также при изменении пропов. Это не совсем так (см. ниже).
Модификация состояния
Компонент всегда подвергается ререндерингу при изменении его состояния. Обычно, это происходит в функции обратного вызова или в хуке useEffect .
Изменения состояния влекут за собой безусловный (непредотвращаемый) ререндеринг
Ререндеринг предка
Компонент подвергается ререндерингу при повторном рендеринге его родительского компонента. Другими словами, когда компонент повторно рендерится, его потомки также ререндерятся.
Ререндеринг «спускается» вниз по дереву компонентов: повторный рендеринг дочернего компонента не влечет ререндеринг его предка (на самом деле, существует несколько пограничных случаев, когда такое возможно: The mystery of React Element, children, parents and re-renders).
Модификация контекста
При изменении значения, передаваемого в провайдер контекста (Context Provider), все компоненты, потребляющие (consume) контекст (эти значения), подвергаются повторному рендерингу, даже если они не используют модифицированные данные. Данный вид ререндеринга нелегко предотвратить, но это возможно (см. ниже).
Модификация хука
Все, что происходит внутри хука, «принадлежит» использующему его компоненту. Здесь действуют те же правила:
- изменение состояния хука влечет безусловный ререндеринг «хостового» (host) компонента;
- если хук потребляет контекст, модификация контекста повлечет безусловный ререндеринг компонента, использующего хук.
Хуки могут вызываться по цепочке. Каждый хук в цепочке принадлежит хостовому компоненту — модификация любого хука влечет безусловный ререндеринг соответствующего компонента.
Изменение пропов (распространенное заблуждение)
До тех пор, пока речь не идет о мемоизированных компонентах, изменения пропов особого значения не имеют.
Модификация пропов означает их обновление родительским компонентом. Это, в свою очередь, означает ререндеринг родительского компонента, влекущий повторный рендеринг всех его потомков.
Изменения пропов становятся важными только при применении различных техник мемоизации ( React.memo , useMemo ).
Предотвращение ререндеринга с помощью композиции
Антипаттерн: создание компонентов в функции рендеринга
Создание компонентов внутри функции рендеринга другого компонента является антипаттерном, который может очень негативно влиять на производительность. React будет повторно монтировать (remount), т. е. уничтожать и создавать с нуля такой компонент при каждом рендеринге, что будет существенно замедлять обычный рендеринг. Это может привести к таким багам, как:
- «вспышки» (flashes) разного контента в процессе рендеринга;
- сброс состояния компонента при каждом рендеринге;
- запуск useEffect без зависимостей при каждом рендеринге;
- потеря фокуса, если компонент находился в этом состоянии и т. д.
Паттерн: перемещение состояния вниз
Данный паттерн используется, когда тяжелый компонент управляет состоянием, которое используется лишь небольшой частью дерева компонентов. Типичным примером может быть открытие/закрытие диалогового/модального окна при нажатии кнопки в сложном компоненте, который рендерит существенную часть страницы.
В этом случае состояние, управляющее видимостью окна, само окно и кнопка, вызывающая обновление состояния окна, могут быть инкапсулированы в отдельном компоненте. Как результат, большой компонент не будет ререндериться при модификации такого состояния.
Паттерн: передача потомков в виде пропов
Это называется «оборачиванием состояния вокруг потомков». Данная техника похожа на предыдущую: изменения состояния инкапсулируются в меньшем компоненте. Разница состоит в том, что состояние используется в качестве обертки медленной часть дерева рендеринга, что облегчает его извлечение. Типичными примерами являются обработчики onScroll или omMouseMove , зарегистрированные на корневом элементе компонента.
В этом случае управление состоянием и использующий его компонент могут быть извлечены в отдельный компонент, а медленный компонент может передаваться ему как children . С точки зрения инкапсулирующего компонента children — обычный проп, поэтому потомки не подвергаются ререндерингу при модификации состояния.
Паттерн: передача компонентов в виде пропов
Данный паттерн очень похож на предыдущий: состояние инкапсулируется внутри меньшего компонента, а большом компонент передается ему как props . Пропы не затрагиваются модификацией состояния, поэтому тяжелый компонент не подвергается ререндерингу.
Может использоваться в случае, когда несколько больших компонентов не зависят от состояния, но не могут быть извлечены как children .
Предотвращение ререндеринга с помощью React.memo
Оборачивание компонента в React.memo останавливает нисходящую цепочку ререндерингов, запущенную где-то выше в дереве компонентов, до тех пор, пока пропы остаются неизменными.
Может использоваться в тяжелых компонентах, не зависящих от источника ререндеринга (состояние, данные и др.).
React.memo : компонент с пропами
Все пропы, которые не являются примитивными значениями, должны мемоизироваться, например, с помощью хука useMemo до передачи компоненту, мемоизируемому с помощью React.memo .
React.memo : компоненты, передаваемыми в виде пропов, или потомки
Компоненты, передаваемые другим компонентам как пропы, или дочерние компоненты должны мемоизироваться с помощью React.memo . Мемоизация родительского компонента работать не будет: потомки и компоненты-пропы — это объекты, которые будут разными при каждом рендеринге.
Повышение производительности ререндеринга с помощью хуков useCallback и useMemo
Антипаттерн: ненужная мемоизация пропов с помощью useCallback/useMemo
Мемоизация пропов сама по себе не предотвращает ререндеринг дочернего компонента. Повторный рендеринг родительского компонента влечет безусловный ререндеринг его потомков независимо от пропов.
Обязательное применение useMemo/useCallback
Если дочерний компонент обернут в React.memo , все пропы, не являющиеся примитивами, должны быть предварительно мемоизированы.
Если компонент использует непримитивные значения в качестве зависимостей таких хуков, как useEffect , useMemo или useCallback , они также должны быть мемоизированы.
Использование useMemo для «дорогих» вычислений
Хук useMemo предназначен для предотвращения дорогих с точки зрения производительности вычислений при повторных рендерингах.
Использование useMemo имеет свою цену: потребляется больше памяти и, как следствие, первоначальный рендеринг становится медленнее. Поэтому его следует применять с умом. В React самые дорогие вычисления производятся при монтировании и обновлении компонентов.
Поэтому типичным примером использования useMemo является мемоизация React-элементов . Такими элементами, как правило, является часть существующего дерева рендеринга или результат генерации такого дерева, например, функция map , возвращающая массив элементов.
Стоимость «чистых» операций, таких как сортировка или фильтрация массива, обычно, являются незначительными по сравнению с обновлениями компонентов.
Повышение производительности ререндеринга списков
Когда речь идет о ререндеринге списков, проп key может иметь важное значение.
Внимание: само по себе предоставление пропа key не повышает производительность рендеринга списка. Для предотвращения ререндеринга элементов списка, они должны оборачиваться в React.memo и следовать другим лучшим практикам.
Значением пропа key должна быть строка, уникальная в пределах компонента и стабильная для элемента. Как правила, для этого используется id или индекс элемента в массиве.
Внимание: индекс следует использовать только в крайнем случае, когда можно быть уверенным, что список является статичным — количество и порядок элементов являются постоянными величинами. Если элементы добавляются/удаляются/вставляются/меняются местами, индексы использовать нельзя.
Использование индексов в качестве ключей элементов динамического списка может привести к:
- багам, связанным с неправильным состоянием компонентов или неуправляемых элементов (таких как поля для ввода);
- снижению производительности, если компоненты обернуты в React.memo .
Антипаттерн: произвольные значения пропа key
Значением key никогда не должны быть рандомные значения. Это приведет к перемонтированию элементов списка при каждом рендеринге, что повлечет за собой:
- очень низкую производительность списка;
- баги, связанные с неправильным состоянием компонентов или неуправляемых элементов (таких как поля для ввода).
Предотвращение ререндеринга, вызываемого контекстом
Мемоизация значения, передаваемого провайдеру
Если провайдер контекста находится не на верхнем уровне приложения и существует вероятность того, что он подвергнется ререндерингу вследствие повторного рендеринга его предков, значение, передаваемое провайдеру, должно быть мемоизировано.
Разделение данных и интерфейсов
Если контекст содержит комбинацию данных и интерфейсов (геттеров и сеттеров), они могут быть разделены на разные провайдеры в рамках одного компонента. Это предотвратит ререндеринг компонентов, которые, например, используют API , но не зависят от данных.
Разделение данных на части
Если контекст управляет несколькими независимыми частями данных (data chunks), его можно разделить на несколько провайдеров. В результате ререндериться буду только компоненты, потребляющие модифицированные данные.
Селекторы контекста
Компонент подвергается безусловному ререндерингу при любом изменении контекста, даже если потребляемое им значение осталось прежним, даже с помощью useMemo .
Однако, можно сымитировать селекторы контекста с помощью компонентов высшего порядка (higher-order components, HOC) и React.memo .
Надеюсь, что вы, как и я, нашли для себя что-то интересное и не зря потратили время. Благодарю за внимание и happy coding!
Оптимизация рендеринга React-компонентов: как не навредить
Всем привет! Если вы используете React для создания UI, то уверена, что вы слышали о таких понятиях, как PureComponent , memo , useCallback и прочих возможностях, которые нам предоставляют создатели библиотеки для оптимизации наших приложений. Разработчики React уже позаботились о том, чтобы обновление DOM было предсказуемым и производительным: преобразования деревьев React-элементов выполняются максимально эффективно с помощью алгоритма согласования (reconciliation). Однако при большом количестве компонентов, глубокой вложенности или неправильной архитектуре количество отрисовок или вызовов функций может заметно увеличиться. Для оптимизации использования ресурсов мы применяем различные приёмы, позволяющие нам, к примеру, избавиться от лишних отрисовок с одинаковыми входными значениями props .
Я рассмотрела частые ошибки при оптимизациях и возможные способы улучшения, сделав акцент на функциональных компонентах. Давайте разберёмся, как не навредить нашему приложению при попытках его улучшить.
Примечание. Возможно, для кого-то перечисленное покажется очевидным. Надеюсь, будут и те, кто найдёт для себя что-то полезное.
Хуки как чёрный ящик: неправильное использование useCallback
Зачастую разработчики представляют себе хуки компонентов как ящик, который принимает на вход определённые параметры и выполняет некую магию.
Одно из убеждений, с которым я зачастую сталкивалась при собеседованиях разработчиков, заключалось в том, что необходимо использовать useCallback для всех колбэков без разбора, в ожидании, что оборачивание функции в хук создаст функцию только один раз, по аналогии с методами класса.
Рассмотрим пример обычного компонента с колбэком, чтобы убедиться, что это не так. Колбэк создаётся и записывается в переменную при каждой отрисовке компонента:
const Component = () => < const onClick = () =>console.log('Clicked'); return >Компонент