useCallback — ключевые понятия, хук мемоизации функций в React

Автор: IT Sectr Опубликовано: 2026-07-04 Время чтения: 9 мин

useCallback — это хук React, который возвращает мемоизированную версию функции, которая не меняется между рендерами, пока не изменятся её зависимости. В отличие от обычного объявления функции внутри компонента (которое создаёт новую функцию при каждом рендере), useCallback стабилизирует ссылку на функцию, что предотвращает ненужные ререндеры дочерних компонентов, оптимизированных через React.memo. По данным React Documentation (2025), useCallback полезен только в паре с React.memo или хуками, зависящими от стабильной ссылки.

Главное

  • useCallback — хук для мемоизации функции, сохраняющий ссылку между рендерами.
  • React.memo — useCallback эффективен только в паре с React.memo для предотвращения ререндеров.
  • Зависимости — функция пересоздаётся только при изменении указанных значений в массиве.
  • Стабильная ссылка — useCallback гарантирует, что ссылка на функцию не изменится без необходимости.
  • Не злоупотребляй — избыточное useCallback без React.memo ухудшает производительность.

Что такое useCallback в React

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 будет только замедлять приложение. Оптимизация должна быть измеримой, а не интуитивной.

jsx
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 и производительность

Широко распространён миф, что useCallback автоматически улучшает производительность. На самом деле, в изоляции (без React.memo) useCallback даже немного замедляет приложение из-за cost сравнения зависимостей. Реальную пользу хук приносит только в трёх сценариях: предотвращение ререндеров React.memo-компонентов, стабильность callback-ов в useEffect и передача в кастомные хуки, которые зависят от reference equality.

Правило простое: пока не обнаружишь проблему с производительностью через React DevTools Profiler — не используй useCallback. React-команда неоднократно подчёркивала, что предварительная оптимизация — корень зла. Сначала напиши чистый код без мемоизации, измерь, найди bottleneck в профилировщике и только потом добавляй useCallback там, где действительно нужно.

jsx
// 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 оправданы. Если дочерний компонент лёгкий — ререндер дешевле, чем сравнение зависимостей.

Когда использовать useCallback

Первый сценарий — React.memo. Если дочерний компонент обёрнут в React.memo и принимает callback-функцию как проп, без useCallback дочерний компонент будет перерисовываться при каждом рендере родителя, даже если его собственные данные не изменились. useCallback стабилизирует ссылку, и React.memo сможет корректно пропустить ререндер.

Второй сценарий — useEffect с callback в зависимостях. Если функция передаётся в массив зависимостей useEffect, каждая новая ссылка будет перезапускать эффект. useCallback гарантирует, что ссылка стабильна, и эффект выполняется только при изменении реальных данных, а не при каждом рендере. Это особенно важно для подписок и запросов.

  • React.memo children — предотвращает ререндер мемоизированных дочерних компонентов при передаче callback-ов.
  • useEffect dependencies — стабилизирует функцию в массиве зависимостей эффекта, предотвращая лишние перезапуски.
  • Кастомные хуки — если хук принимает коллбэк и зависит от его reference equality, useCallback обязателен.
  • Context value — если в value контекста передаётся функция, useMemo с useCallback стабилизирует ссылку.
jsx
// 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 vs useMemo

Главное различие между 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)Дорогие вычисления, мемоизация объектов
jsx
// These are equivalent:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);

Типичные ошибки с useCallback

Самая распространённая ошибка — бессмысленное оборачивание всех функций в useCallback без React.memo на дочерних компонентах. Если дочерний компонент не обёрнут в React.memo, он всё равно будет ререндериться при каждом рендере родителя, независимо от того, меняется ссылка на callback или нет. useCallback без React.memo — это затраты без выгоды.

  • useCallback без React.memo — дочерний компонент всё равно ререндерится; оптимизация бессмысленна.
  • Забытые зависимости — если внутри callback используется переменная, не указанная в deps, callback содержит stale closure.
  • Избыточная мемоизация — оборачивание каждой функции, включая тривиальные onClick с console.log.
  • Объекты и массивы в зависимостях — новая ссылка при каждом рендере считается изменением, мемоизация бесполезна.
jsx
// ❌ 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?

Нет. useCallback имеет смысл только в трёх случаях: дочерний компонент обёрнут в React.memo, функция используется в массиве зависимостей useEffect или функция передаётся в кастомный хук, зависящий от reference equality. В остальных случаях useCallback добавляет overhead без пользы. React-команда рекомендует сначала писать без оптимизаций и добавлять их по результатам профилирования.

Что быстрее — useCallback или новая функция на каждый рендер?

Для простых компонентов — новая функция на каждый рендер немного быстрее, так как useCallback тратит ресурсы на сравнение зависимостей и аллокацию массива. Для компонентов с глубокими деревьями React.memo — useCallback выигрывает, предотвращая ререндер тысяч дочерних элементов. Измеряй и сравнивай, а не гадай — используй React DevTools Profiler для объективной оценки.

Можно ли использовать useCallback с async функциями?

Да, useCallback работает с async-функциями точно так же, как с синхронными. Хук мемоизирует саму функцию, а результат (Promise) возвращается каждый раз при вызове. Асинхронная функция внутри useCallback — распространённый паттерн для стабильных callback-ов загрузки данных, которые используются в useEffect: const fetchData = useCallback(async (id) => {...}, []).

Как отладить проблему с useCallback и React.memo?

Используй React DevTools Profiler — он покажет, какие компоненты ререндерятся и почему. Для программной проверки добавь console.log или useWhyDidYouUpdate — библиотеку, которая логирует причину ререндера. Основные причины: изменился проп (включая callback-ссылку), изменился state или контекст. Если useCallback не помогает — проверь, что все зависимости указаны корректно.

Как передать useCallback в дочерний компонент без React.memo?

Даже без React.memo useCallback может быть полезен в комбинации с useMemo для value контекста. Если ты передаёшь объект с функциями в Context.Provider, оберни создание объекта в useMemo, а каждую функцию — в useCallback. Это предотвратит ререндер всех потребителей контекста при изменении одной из функций. Но для прямой передачи callback-ов в пропсы без React.memo пользы от useCallback нет.

Итоги

  • useCallback — хук для мемоизации функции, возвращающий стабильную ссылку до изменения зависимостей.
  • React.memo synergy — useCallback эффективен только в паре с React.memo на дочерних компонентах.
  • Сравнение зависимостей — через Object.is; объекты/массивы в deps ломают мемоизацию.
  • Измеряй, не гадай — добавляй useCallback только после обнаружения bottleneck через профилировщик.
  • Stable closure — включай все используемые переменные в массив зависимостей, иначе — stale closure.
  • useCallback vs useMemo — useCallback мемоизирует функцию, useMemo — результат вычисления.
  • Избыточность — более 90% случаев использования useCallback в реальных проектах — premature optimization.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также