useCallback
useCallback
— это хук в React, который позволяет кешировать функции между повторными рендерингами.
const cachedFn = useCallback(fn, dependencies)
Справочник
useCallback(fn, dependencies)
Вызовите useCallback
на верхнем уровне вашего компонента, чтобы кешировать функцию между повторными рендерами:
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 даст вам ту же функцию, еслиdependencies
не изменились. В противном случае он вернёт функцию, переданную при текущем рендере, и сохранит её для возможного повторного использования. React не вызывает вашу функцию. Он возвращает её вам, чтобы вы могли решить, когда и как её вызывать. -
dependencies
: Список всех реактивных значений, на которые ссылается кодfn
. К реактивным значениям относятся пропсы, состояние и все переменные и функции, объявленные непосредственно в теле компонента. Если ваш линтер настроен для использования с React, он проверит, что каждое реактивное значение правильно указано как зависимость. Список зависимостей должен иметь постоянное количество элементов и быть записан примерно так:[dep1, dep2, dep3]
. React будет сравнивать каждую зависимость с её предыдущим значением, используя алгоритм сравненияObject.is
.
Возвращаемое значение
При первом рендере useCallback
возвращает функцию fn
, которую вы передали.
Во время последующих рендеров он либо возвращает уже сохранённую функцию fn
с последнего рендера (если зависимости не изменились), либо возвращает функцию fn
, переданную при текущем рендере.
Предостережения
useCallback
— это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, выделите компонент и перенесите состояние туда.- React не выбрасывает кешированную функцию без веской причины. Например, в режиме разработки React сбрасывает кеш при изменении файла вашего компонента. В разработке и в продакшене кеш сбрасывается, если ваш компонент приостановлен во время начальной загрузки. В будущем React может добавить функции, которые будут использовать сброс кеша — например, встроенная поддержка виртуализированных списков может потребовать сброса кеша для элементов, которые выходят за пределы области видимости. Это должно соответствовать вашим ожиданиям при использовании
useCallback
для оптимизации производительности. В противном случае, использование состояния или рефа могут быть более подходящими.
Использование
Пропуск повторного рендеринга компонентов
Когда вы оптимизируете производительность рендеринга, иногда нужно кешировать функции, которые вы передаёте дочерним компонентам. Сначала рассмотрим синтаксис, как это сделать, а затем посмотрим, в каких случаях это полезно.
Чтобы кешировать функцию между рендерами вашего компонента, оберните её в хук useCallback
:
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
:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
Вы заметили, что при переключении пропа theme
приложение на мгновение зависает, но если убрать <ShippingForm />
из вашего JSX, оно работает быстро. Это говорит о том, что стоит попытаться оптимизировать компонент ShippingForm
.
По умолчанию, когда компонент повторно рендерится, React рекурсивно отрендерит снова все его дочерние компоненты. Поэтому, когда ProductPage
рендерится с другим theme
, компонент ShippingForm
тоже повторно рендерится. Это нормально для компонентов, которые не требуют больших вычислений при рендере. Но если повторный рендер медленный, можно сказать ShippingForm
пропустить повторный рендеринг, если его пропсы такие же, как при последнем рендере, обернув его в memo
:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
С этим изменением ShippingForm
будет пропускать повторный рендер, если все его пропсы останутся такими же , как при последнем рендере. Вот когда кеширование функции становится важным! Предположим, вы определили handleSubmit
без useCallback
:
function ProductPage({ productId, referrer, theme }) {
// Каждый раз, когда тема изменяется, это будет другая функция...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... таким образом, пропсы ShippingForm никогда не будут одинаковыми, и он будет повторно рендериться каждый раз. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
В JavaScript function () {}
or () => {}
всегда создаёт новую функцию, так же как литерал объекта {}
всегда создаёт новый объект. Обычно это не проблема, но это означает, что пропсы ShippingForm
никогда не будут одинаковыми, и ваша оптимизация с memo
не сработает. Здесь на помощь приходит useCallback
:
function ProductPage({ productId, referrer, theme }) {
// Сообщите React, чтобы кешировать вашу функцию между повторными рендерами...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // // ...пока эти зависимости не изменятся...
return (
<div className={theme}>
{/* ...ShippingForm будет получать те же пропсы и может пропускать повторный рендер */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Оборачивая handleSubmit
в useCallback
, вы гарантируете, что это одна и та же функция между повторными рендерами (пока зависимости не изменятся). Вам не нужно оборачивать функцию в useCallback
, если на это нет конкретной причины. В этом примере причина в том, что вы передаёте её в компонент, обёрнутый в memo
, что позволяет ему пропускать повторные рендеры. Есть и другие причины использовать useCallback
, которые описаны далее на этой странице.
Deep Dive
Вы часто увидите useMemo
вместе с useCallback
. Они оба полезны при оптимизации дочернего компонента. Они позволяют вам мемоизировать (или, другими словами, кешировать) что-то, что вы передаёте вниз по иерархии:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // // Вызывает вашу функцию и кеширует её результат
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Кеширует саму вашу функцию
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
так:
// Упрощённая реализация (внутри React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Deep Dive
Если ваше приложение похоже на этот сайт, и большинство взаимодействий грубые (например, замена страницы или целого раздела), мемоизация обычно не нужна. С другой стороны, если ваше приложение похоже на редактор рисунков, и большинство взаимодействий детализированы (например, перемещение фигур), мемоизация может быть очень полезной.
Кеширование функции с помощью useCallback
полезно в нескольких случаях:
- Вы передаёте её как проп компоненту, обёрнутому в
memo
. Вы хотите пропустить повторный рендер, если значение не изменилось. Мемоизация позволяет вашему компоненту повторно рендериться, только если зависимости изменились. - Функция, которую вы передаёте, позже используется как зависимость в каком-то хуке. Например, другая функция, обёрнутая в
useCallback
, зависит от неё, или вы зависите от этой функции вuseEffect.
Нет смысла оборачивать функцию в useCallback
в других случаях. Это не принесёт значительного вреда, поэтому некоторые команды решают не думать о конкретных случаях и мемоизируют как можно больше. Недостатком является то, что код становится менее читаемым. Кроме того, не всякая мемоизация эффективна: одно значение, которое «всегда новое», достаточно, чтобы сломать мемоизацию для всего компонента.
Обратите внимание, что useCallback
не предотвращает создание функции. Вы всегда создаёте функцию (и это нормально!), но React игнорирует её и возвращает кешированную функцию, если ничего не изменилось.
На практике можно сделать большую часть мемоизации ненужной, следуя нескольким принципам:
- Когда компонент оборачивает другие компоненты, пусть он принимает JSX как дочерний. Если обёрточный компонент обновляет своё состояние, React знает, что его дети не нужно повторно рендерить.
- Предпочитайте локальное состояние и не поднимайте состояние выше, чем это необходимо. Не держите временное состояние, такое как формы или состояние наведения, на верхнем уровне дерева или в глобальной библиотеке состояния.
- Держите логику рендеринга чистой. Если повторный рендеринг компонента вызывает проблему или заметные визуальные артефакты, это ошибка в вашем компоненте! Исправьте ошибку вместо добавления мемоизации.
- Избегайте ненужных эффектов, которые обновляют состояние. Большинство проблем с производительностью в приложениях React вызвано цепочками обновлений, исходящими от эффектов, которые заставляют ваши компоненты рендериться снова и снова.
- Попытайтесь удалить ненужные зависимости из ваших эффектов. Например, вместо мемоизации часто проще переместить какой-то объект или функцию внутрь эффекта или за пределы компонента.
Если конкретное взаимодействие все ещё кажется медленным, используйте профайлер в React Developer Tools, чтобы определить, какие компоненты больше всего выиграют от мемоизации, и добавьте мемоизацию там, где это необходимо. Эти принципы делают ваши компоненты легче для отладки и понимания, поэтому хорошо следовать им в любом случае. В долгосрочной перспективе мы исследуем возможность автоматической мемоизации, чтобы решить эту проблему раз и навсегда.
Example 1 of 2: Пропуск повторного рендеринга с помощью useCallback
и memo
В этом примере компонент ShippingForm
искусственно замедлен, чтобы вы могли увидеть, что происходит, когда React-компонент действительно медленный. Попробуйте увеличить счётчик и переключить тему.
Увеличение счётчика ощущается медленным, потому что это вынуждает замедленный ShippingForm
повторно рендериться. Это ожидаемо, так как счётчик изменился, и нужно отобразить новый выбор пользователя на экране.
Теперь попробуйте переключить тему. Благодаря useCallback
вместе с memo
, это происходит быстро, несмотря на искусственное замедление! ShippingForm
пропустил повторный рендер, потому что функция handleSubmit
не изменилась. Функция handleSubmit
не изменилась, потому что productId
и referrer
(зависимости вашего useCallback
) не изменились с момента последнего рендера.
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) { // Представьте, что это отправляет запрос... console.log('POST /' + url); console.log(data); }
Обновление состояния из мемоизированного колбэка
Иногда может потребоваться обновить состояние на основе предыдущего состояния из мемоизированного колбэка.
Эта функция handleAddTodo
указывает todos
как зависимость, потому что она вычисляет новые todos
:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Обычно вы хотите, чтобы у мемоизированных функций было как можно меньше зависимостей. Когда вы читаете состояние только для вычисления следующего состояния, вы можете удалить эту зависимость, передавая функцию обновления:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ Нет необходимости в зависимости от todos
// ...
Здесь, вместо того чтобы делать todos
зависимостью и считывать его внутри, вы передаёте React инструкцию о том, как обновить состояние (todos => [...todos, newTodo]
). Подробнее о функциях обновления.
Предотвращение слишком частого срабатывания эффекта
Иногда может понадобиться вызвать функцию из эффекта:
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
как зависимость, это приведёт к постоянному повторному подключению эффекта к чат-комнате:
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Проблема: Эта зависимость изменяется при каждом рендере
// ...
Чтобы решить эту проблему, вы можете обернуть функцию, которую нужно вызвать из эффекта, в useCallback
:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Изменяется только при изменении roomId
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Изменяется только при изменении createOptions
// ...
Это гарантирует, что функция createOptions
остаётся той же между рендерами, если roomId
не изменился. Однако, ещё лучше убрать необходимость в зависимости от функции. Переместите свою функцию внутрь эффекта:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ Нет необходимости в useCallback или зависимостях функции!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Изменяется только при изменении roomId
// ...
Теперь ваш код проще и не нуждается в useCallback
. Узнайте больше об удалении зависимостей эффекта.
Оптимизация пользовательского хука
Если вы пишете пользовательский хук, рекомендуется оборачивать любые функции, которые он возвращает, в useCallback
:
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,
};
}
Это гарантирует, что потребители вашего хука могут оптимизировать свой код при необходимости.
Устранение неполадок
Каждый раз, когда мой компонент рендерится, useCallback
возвращает другую функцию
Убедитесь, что вы указали массив зависимостей в качестве второго аргумента!
Если вы забудете массив зависимостей, useCallback
будет возвращать новую функцию каждый раз:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Возвращает новую функцию каждый раз: нет массива зависимостей
// ...
Вот исправленная версия с передачей массива зависимостей в качестве второго аргумента:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Не возвращает новую функцию без необходимости
// ...
Если это не помогает, проблема может быть в том, что хотя бы одна из ваших зависимостей отличается от предыдущего рендера. Вы можете отладить эту проблему, вручную выводя зависимости в консоль:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Вы можете щёлкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать “Store as global variable” для обоих. Предположим, первый сохранён как temp1
, а второй как temp2
. Затем вы можете использовать консоль браузера, чтобы проверить, являются ли каждая зависимость в обоих массивах одинаковыми:
Object.is(temp1[0], temp2[0]); // Первая зависимость одинаковая в обоих массивах?
Object.is(temp1[1], temp2[1]); // Вторая зависимость одинаковая в обоих массивах?
Object.is(temp1[2], temp2[2]); // ... и так далее для каждой зависимости ...
Когда вы найдёте зависимость, нарушающую мемоизацию, либо найдите способ удалить её, либо мемоизируйте её также.
Мне нужно вызвать useCallback
для каждого элемента списка в цикле, но это не разрешено
Предположим, что компонент Chart
обёрнут в memo
. Вы хотите пропустить повторный рендеринг каждого Chart
в списке, когда компонент ReportList
рендерится заново. Однако вы не можете вызывать useCallback
в цикле:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 Вы не можете вызывать useCallback в цикле вот так:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Вместо этого выделите компонент для отдельного элемента и поместите useCallback
там:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Вызовите useCallback на верхнем уровне:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Альтернативно, вы можете убрать useCallback
в последнем фрагменте кода и вместо этого обернуть Report
в memo
. Если проп item
не изменяется, Report
пропустит повторный рендеринг, поэтому Chart
также пропустит повторный рендеринг:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});