Если ты добавляешь функцию в массив зависимостей useEffect
и получаешь бесконечное количество повторных отрисовок - пришло время использовать useCallback
!
Синтаксис:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
useCallback
возвращает новую версию функции только при изменении ее зависимостей. В приведенном выше примере это работает только при изменении a
или b
.
Это означает, что даже при повторном рендеринге компонента ты можешь быть уверен, что функция, заключенная в useCallback
, не будет повторно объявлена, что предотвратит ужасный бесконечный цикл повторного рендеринга / useEffect
.
Прежде чем мы пойдем в глубь useCallback
, давай быстро освежим в памяти компоненты React.
Повторение
Вот компонент React, который отображает дочерний компонент. Иногда может произойти что-то, что приведет к повторному рендерингу компонента (например, изменение свойств props
или вызов useState
).
function SomeComponent(props) {
return <DataFetcher />;
}
Теперь, если мы хотим передать <DataFetcher />
props, например функцию, которая генерирует URL-адрес для выборки данных, можем определить функцию следующим образом:
function SomeComponent(props){
function getUrl(id){
return "https://some-api-url.com/api/" + id + "/"
}
return <DataFetcher getUrl={getUrl}>
}
Прежде чем беспокоиться о хуках, мы могли бы определить функцию в классе и передать ее дочерним элементам, которые могли бы без проблем запускать this.getUrl(id)
. Однако теперь, когда мы находимся в функциональном компоненте, то как мы определили getUrl
, означает нечто иное.
Поскольку по умолчанию весь код SomeComponent
повторно запускается при рендеринге, getUrl
переопределяется при каждом рендеринге.
Если DataFetcher
затем использует getUrl
как часть хука useEffect
, даже если ты добавляешь getUrl
в массив зависимостей, твой useEffect
будет запускаться каждый рендер.
useEffect(() => {
fetchDataToDoSomething(getUrl);
}, [getUrl]); // 🔴 повторно запускает useEffect каждый рендер
Как остановить запуск useEffect каждый рендер?
Вернувшись в SomeComponent
, у нас есть два варианта решения этой проблемы (при условии, что мы не можем просто переместить getUrl
в проблемный хук useEffect
).
Старомодный способ: переместить getUrl
за пределы компонента, чтобы он не объявлялся повторно при каждом рендере:
function getUrl(id){
return "https://some-api-url.com/api/" + id + "/"
}
function SomeComponent(props){
return <DataFetcher getUrl={getUrl}>
}
Способ Hooks, заключающийся в том, чтобы обернуть getUrl
в useCallback
:
function SomeComponent(props){
const getUrl = useCallback(function (id) {
return "https://some-api-url.com/api/" + id + "/";
}, []); // <-- Обрати внимание, что в этом случае ты не можешь добавить id в массив deps.
return <DataFetcher getUrl={getUrl}>
}
Конечно, это довольно упрощенный пример, показывающий, что ты можешь сделать, чтобы исправить гораздо более сложный код.
Я не предлагаю тебе использования useCallback
/ useMemo
везде или избегать их.
Ключевой вывод заключается в том, что useCallback
возвращает новую версию функции только при изменении её зависимостей, избавляя дочерние компоненты от автоматического повторного рендеринга каждый раз, при рендеринге родительского компонента. Это особенно полезно при использовании с useEffect
, поскольку можно безопасно добавлять функции, заключенные в useCallback
, в массив зависимостей, не опасаясь бесконечных повторных отрисовок.