useCallback — это хук React, который возвращает мемоизированную версию функции, которая не меняется между рендерами, пока не изменятся её зависимости. В отличие от обычного объявления функции внутри компонента (которое создаёт новую функцию при каждом рендере), useCallback стабилизирует ссылку на функцию, что предотвращает ненужные ререндеры дочерних компонентов, оптимизированных через React.memo. По данным React Documentation (2025), useCallback полезен только в паре с React.memo или хуками, зависящими от стабильной ссылки.
Главное
useCallback — это хук, добавленный в React 16.8, который мемоизирует функцию: возвращает одну и ту же ссылку до тех пор, пока зависимости не изменятся. Без useCallback каждое объявление функции внутри компонента создаёт новый объект функции при каждом рендере. Для примитивов это незаметно, но при передаче таких callback-ов в дочерние компоненты, оптимизированные через React.memo, каждый новый reference вызывает ререндер дочернего компонента.
Синтаксически useCallback эквивалентен useMemo для функции: useCallback(fn, deps) — это сокращение для useMemo(() => fn, deps). React хранит мемоизированную функцию во внутреннем хранилище fiber-узла и сравнивает зависимости при каждом рендере. Если зависимости не изменились (Object.is для каждого элемента), возвращается предыдущая функция.
По данным React Documentation — useCallback (2025), не стоит оборачивать каждую функцию в useCallback. Хук имеет свою цену: вызов хука, сравнение зависимостей и аллокация памяти под массив зависимостей. Если компонент простой и не имеет глубоких деревьев с React.memo — useCallback будет только замедлять приложение. Оптимизация должна быть измеримой, а не интуитивной.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Stable reference — same function until deps change
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
Мемоизация в useCallback основана на кэшировании результата вызова функции. React сохраняет замыкание, созданное в момент первого рендера, и возвращает его при последующих рендерах, пока зависимости остаются неизменными. Внутри fiber-узла каждый вызов useCallback создаёт узел в связном списке хуков, где хранятся предыдущие зависимости и мемоизированное значение.
Процесс сравнения зависимостей выполняется строго через Object.is — это поверхностное сравнение, без глубокой проверки объектов или массивов. Если в зависимостях объект или массив, новая ссылка при каждом рендере будет считаться изменением. Поэтому в массиве зависимостей нужно передавать примитивные значения или стабильные ссылки (например, из useRef или useMemo).
По данным React Core Team — Optimization Guide (2024), стоимость мемоизации включает три компонента: аллокацию массива зависимостей на каждый рендер, обход и сравнение элементов через Object.is и потенциальный overhead от сборки мусора при пересоздании. Для компонента с сотнями useCallback-обёрток это может стать заметным — поэтому селективность в использовании хука критична.
| Сценарий | Без useCallback | С useCallback |
|---|---|---|
| Создание функции | Новая на каждый рендер | Та же при стабильных deps |
| Передача в React.memo | Ребёнок ререндерится | Ребёнок не ререндерится |
| В массиве useEffect | Эффект перезапускается | Эффект стабилен |
| Overhead | Минимальный | Сравнение зависимостей + память |
Широко распространён миф, что useCallback автоматически улучшает производительность. На самом деле, в изоляции (без React.memo) useCallback даже немного замедляет приложение из-за cost сравнения зависимостей. Реальную пользу хук приносит только в трёх сценариях: предотвращение ререндеров React.memo-компонентов, стабильность callback-ов в useEffect и передача в кастомные хуки, которые зависят от reference equality.
Правило простое: пока не обнаружишь проблему с производительностью через React DevTools Profiler — не используй useCallback. React-команда неоднократно подчёркивала, что предварительная оптимизация — корень зла. Сначала напиши чистый код без мемоизации, измерь, найди bottleneck в профилировщике и только потом добавляй useCallback там, где действительно нужно.
// Measurable optimization: Child is wrapped in React.memo
const Child = React.memo(({ onClick }) => {
console.log('Child re-rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}
По данным Dan Abramov — Before You memo() (2024), более 90% случаев использования useCallback в open-source проектах — избыточны. Разработчики оборачивают каждую функцию «на всякий случай», не измеряя эффект. Альтернатива: если дочерний компонент тяжёлый и его ререндер дорог — React.memo + useCallback оправданы. Если дочерний компонент лёгкий — ререндер дешевле, чем сравнение зависимостей.
Первый сценарий — React.memo. Если дочерний компонент обёрнут в React.memo и принимает callback-функцию как проп, без useCallback дочерний компонент будет перерисовываться при каждом рендере родителя, даже если его собственные данные не изменились. useCallback стабилизирует ссылку, и React.memo сможет корректно пропустить ререндер.
Второй сценарий — useEffect с callback в зависимостях. Если функция передаётся в массив зависимостей useEffect, каждая новая ссылка будет перезапускать эффект. useCallback гарантирует, что ссылка стабильна, и эффект выполняется только при изменении реальных данных, а не при каждом рендере. Это особенно важно для подписок и запросов.
// useCallback for stable useEffect dependency
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // stable reference, never re-creates
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // effect runs only when props.id changes
Главное различие между useCallback и useMemo в том, что мемоизирует каждый из них. useCallback мемоизирует функцию: useCallback(fn, deps) возвращает fn (ту же самую или предыдущую версию). useMemo мемоизирует результат вызова функции: useMemo(() => computeExpensive(a, b), [a, b]) возвращает вычисленное значение, а не функцию.
Технически useCallback — это синтаксический сахар над useMemo: useCallback(fn, deps) эквивалентен useMemo(() => fn, deps). Этот синтаксис существует только для читаемости — чтобы разработчик явно видел, что мемоизируется именно функция, а не значение. Разницы в производительности между useCallback и useMemo с функцией нет — они генерируют одинаковый код.
| Хук | Мемоизирует | Синтаксис | Использование |
|---|---|---|---|
| useCallback | Функцию (ссылку) | useCallback(fn, deps) | Callback-и для дочерних компонентов |
| useMemo | Результат вычисления | useMemo(() => value, deps) | Дорогие вычисления, мемоизация объектов |
// These are equivalent:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
Самая распространённая ошибка — бессмысленное оборачивание всех функций в useCallback без React.memo на дочерних компонентах. Если дочерний компонент не обёрнут в React.memo, он всё равно будет ререндериться при каждом рендере родителя, независимо от того, меняется ссылка на callback или нет. useCallback без React.memo — это затраты без выгоды.
// ❌ Useless: no React.memo on child
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child still re-renders without React.memo
// ❌ Stale closure: missing dependency
const handle = useCallback(() => {
console.log(count); // count is always 0 — stale closure!
}, []);
// ✅ Correct: include dependencies
const handle = useCallback(() => {
console.log(count);
}, [count]);
Проблема stale closure в useCallback решается указанием всех используемых переменных в массиве зависимостей. eslint-plugin-react-hooks с exhaustive-deps автоматически проверяет, что все переменные из тела callback-а присутствуют в массиве. Если callback использует setState, который не меняется между рендерами, можно безопасно указать его в deps — React гарантирует стабильность setState.
Часто задаваемые вопросы
Нет. useCallback имеет смысл только в трёх случаях: дочерний компонент обёрнут в React.memo, функция используется в массиве зависимостей useEffect или функция передаётся в кастомный хук, зависящий от reference equality. В остальных случаях useCallback добавляет overhead без пользы. React-команда рекомендует сначала писать без оптимизаций и добавлять их по результатам профилирования.
Для простых компонентов — новая функция на каждый рендер немного быстрее, так как useCallback тратит ресурсы на сравнение зависимостей и аллокацию массива. Для компонентов с глубокими деревьями React.memo — useCallback выигрывает, предотвращая ререндер тысяч дочерних элементов. Измеряй и сравнивай, а не гадай — используй React DevTools Profiler для объективной оценки.
Да, useCallback работает с async-функциями точно так же, как с синхронными. Хук мемоизирует саму функцию, а результат (Promise) возвращается каждый раз при вызове. Асинхронная функция внутри useCallback — распространённый паттерн для стабильных callback-ов загрузки данных, которые используются в useEffect: const fetchData = useCallback(async (id) => {...}, []).
Используй React DevTools Profiler — он покажет, какие компоненты ререндерятся и почему. Для программной проверки добавь console.log или useWhyDidYouUpdate — библиотеку, которая логирует причину ререндера. Основные причины: изменился проп (включая callback-ссылку), изменился state или контекст. Если useCallback не помогает — проверь, что все зависимости указаны корректно.
Даже без React.memo useCallback может быть полезен в комбинации с useMemo для value контекста. Если ты передаёшь объект с функциями в Context.Provider, оберни создание объекта в useMemo, а каждую функцию — в useCallback. Это предотвратит ререндер всех потребителей контекста при изменении одной из функций. Но для прямой передачи callback-ов в пропсы без React.memo пользы от useCallback нет.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также