useCallback в React — мемоизация функций и оптимизация ре-рендеров

16 июня 2026
Автор

Антон Ларичев

Введение

React пересоздаёт все функции внутри компонента при каждом рендере. В большинстве случаев это не проблема — функции занимают мало памяти и создаются быстро. Но когда функция передаётся дочернему компоненту или указывается как зависимость в useEffect или useMemo, пересоздание функции приводит к лишним ре-рендерам и бесконечным циклам.

Хук useCallback решает эту задачу — он мемоизирует функцию и возвращает ту же ссылку между рендерами, пока список зависимостей не изменился.

Если вы хотите глубоко разобраться с оптимизацией React-компонентов и другими хуками — приходите на курс Основы React, React Router и Redux Toolkit. На курсе 177 уроков, AI-тренажёры для практики, живое ревью наставника и еженедельные встречи с менторами.

Синтаксис useCallback

import { useCallback } from 'react';

const memoizedFn = useCallback(
  () => {
    // тело функции
  },
  [dependency1, dependency2] // массив зависимостей
);

Хук принимает два аргумента:

  • Функцию — то, что нужно мемоизировать.
  • Массив зависимостей — список значений, при изменении которых функция пересоздаётся.

useCallback возвращает мемоизированную версию переданной функции.

Как работают зависимости

React сравнивает элементы массива зависимостей с помощью алгоритма Object.is (поверхностное сравнение). Если все элементы совпадают с предыдущим рендером — возвращается та же ссылка. Если хотя бы один изменился — функция создаётся заново.

import { useState, useCallback } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const [page, setPage] = useState(1);

  // Функция пересоздаётся только при изменении query или page
  const handleSearch = useCallback(() => {
    console.log(`Поиск: ${query}, страница: ${page}`);
    fetchResults(query, page);
  }, [query, page]); // зависимости явно перечислены

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Найти</button>
    </div>
  );
}

Если передать пустой массив [], функция создаётся один раз при монтировании компонента и больше не обновляется. Это подходит для функций, которые не используют значения из области видимости компонента.

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

Передача функции в React.memo-компонент

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

import { useState, useCallback, memo } from 'react';

// Дочерний компонент обёрнут в memo
const Button = memo(({ onClick, label }: { onClick: () => void; label: string }) => {
  console.log(`Button "${label}" re-rendered`);
  return <button onClick={onClick}>{label}</button>;
});

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Без useCallback — Button ре-рендерится при изменении text
  // С useCallback — Button рендерится только один раз
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []); // не зависит от count благодаря функциональному обновлению

  return (
    <div>
      <p>Счётчик: {count}</p>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={increment} label="Увеличить" />
    </div>
  );
}

Функция как зависимость useEffect

Если функция передаётся как зависимость в useEffect, без useCallback это создаёт бесконечный цикл: useEffect срабатывает → состояние обновляется → рендер → новая функция → useEffect снова.

import { useState, useEffect, useCallback } from 'react';

function UserCard({ userId }: { userId: string }) {
  const [user, setUser] = useState<{ name: string; email: string } | null>(null);
  const [loading, setLoading] = useState(false);

  // Мемоизируем функцию — зависимость useEffect стабильна
  const loadUser = useCallback(async () => {
    setLoading(true);
    try {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
    } finally {
      setLoading(false);
    }
  }, [userId]); // пересоздаётся только при смене userId

  useEffect(() => {
    loadUser();
  }, [loadUser]); // безопасная зависимость

  if (loading) return <p>Загрузка...</p>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={loadUser}>Обновить</button>
    </div>
  );
}

Передача функции в кастомный хук

Если кастомный хук принимает коллбэк и использует его внутри useEffect, передаваемую функцию стоит мемоизировать.

import { useEffect, useCallback } from 'react';

// Кастомный хук с коллбэком
function useInterval(callback: () => void, delay: number) {
  useEffect(() => {
    const id = setInterval(callback, delay);
    return () => clearInterval(id);
  }, [callback, delay]);
}

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Мемоизируем — иначе useInterval пересоздаёт интервал при каждом рендере
  const tick = useCallback(() => {
    setSeconds(s => s + 1);
  }, []); // пустые зависимости — используем функциональное обновление

  useInterval(tick, 1000);

  return <p>Прошло секунд: {seconds}</p>;
}

Отличие useCallback от useMemo

Оба хука мемоизируют значения, но разного характера:

Хук Что мемоизирует Пример
useCallback(fn, deps) Функцию (саму функцию как значение) Обработчики событий, коллбэки
useMemo(() => value, deps) Результат вызова функции Отфильтрованные массивы, объекты, вычисления

По сути useCallback(fn, deps) — это сокращение для useMemo(() => fn, deps):

// Эти два варианта делают одно и то же:
const memoFn = useCallback(() => doWork(a, b), [a, b]);
const memoFn = useMemo(() => () => doWork(a, b), [a, b]);

// useMemo для мемоизации результата вычисления:
const sortedItems = useMemo(() => [...items].sort(), [items]);

// useCallback для мемоизации функции:
const handleSort = useCallback(() => {
  setSorted(prev => !prev);
}, []);

Если нужно мемоизировать функцию — используйте useCallback. Если нужно мемоизировать результат вычисления — используйте useMemo.

Если вы хотите разобраться, как грамотно сочетать useCallback, useMemo и React.memo в реальных проектах — на курсе Основы React, React Router и Redux Toolkit мы разбираем эти темы на практических примерах с код-ревью.

Пример оптимизации списка

Рассмотрим типичную задачу: список товаров с обработчиками на каждом элементе.

import { useState, useCallback, memo } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
  inCart: boolean;
}

// Мемоизированный элемент списка
const ProductItem = memo(({
  product,
  onAdd,
  onRemove,
}: {
  product: Product;
  onAdd: (id: number) => void;
  onRemove: (id: number) => void;
}) => {
  console.log(`ProductItem ${product.id} re-rendered`);
  return (
    <li>
      <span>{product.name} — {product.price} ₽</span>
      {product.inCart ? (
        <button onClick={() => onRemove(product.id)}>Убрать из корзины</button>
      ) : (
        <button onClick={() => onAdd(product.id)}>В корзину</button>
      )}
    </li>
  );
});

function ProductList({ products }: { products: Product[] }) {
  const [cart, setCart] = useState<Set<number>>(new Set());

  // Стабильные ссылки — ProductItem не ре-рендерится при изменении cart
  const handleAdd = useCallback((id: number) => {
    setCart(prev => new Set([...prev, id]));
  }, []);

  const handleRemove = useCallback((id: number) => {
    setCart(prev => {
      const next = new Set(prev);
      next.delete(id);
      return next;
    });
  }, []);

  const enriched = products.map(p => ({ ...p, inCart: cart.has(p.id) }));

  return (
    <ul>
      {enriched.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAdd={handleAdd}
          onRemove={handleRemove}
        />
      ))}
    </ul>
  );
}

Без useCallback при изменении корзины (добавлении/удалении товара) ре-рендерились бы все элементы списка. С useCallback ре-рендерится только тот ProductItem, чьё свойство inCart изменилось.

Когда не нужен useCallback

useCallback добавляет накладные расходы: React должен создать замыкание, сохранить зависимости и сравнить их при каждом рендере. Для простых случаев это может быть дороже, чем просто пересоздать функцию.

Не используйте useCallback:

  • Для обычных обработчиков событий, которые не передаются никуда дальше.
  • Если дочерний компонент не обёрнут в React.memo — мемоизация бесполезна.
  • Если зависимости меняются при каждом рендере — функция будет пересоздаваться в любом случае.
// Нет смысла — handleClick нигде не используется как зависимость
// и не передаётся в memo-компонент
const handleClick = useCallback(() => {
  setOpen(true);
}, []);

// Лучше так — просто и читаемо
const handleClick = () => setOpen(true);

Частые ошибки

Ошибка 1: Пропущенная зависимость

Самая распространённая ошибка — использование переменной из области видимости без добавления её в массив зависимостей. Функция будет использовать устаревшее значение.

// НЕПРАВИЛЬНО: userId используется, но не указан в зависимостях
const fetchData = useCallback(async () => {
  const res = await fetch(`/api/users/${userId}`);
  // userId никогда не обновится в этой функции!
}, []);

// ПРАВИЛЬНО: добавляем все используемые значения
const fetchData = useCallback(async () => {
  const res = await fetch(`/api/users/${userId}`);
  const data = await res.json();
  setUser(data);
}, [userId]);

Используйте правило ESLint exhaustive-deps из пакета eslint-plugin-react-hooks — оно автоматически предупреждает о пропущенных зависимостях.

Ошибка 2: Объект или массив в зависимостях

Объекты и массивы создаются заново при каждом рендере, поэтому их не стоит включать в зависимости напрямую — функция будет пересоздаваться всегда.

// НЕПРАВИЛЬНО: filters — новый объект при каждом рендере
const search = useCallback(() => {
  fetchResults(query, filters);
}, [query, filters]); // filters всегда "новый" объект

// ПРАВИЛЬНО: мемоизируем объект через useMemo
const memoFilters = useMemo(() => ({ category, minPrice, maxPrice }), [category, minPrice, maxPrice]);

const search = useCallback(() => {
  fetchResults(query, memoFilters);
}, [query, memoFilters]);

Ошибка 3: useCallback для всего подряд

Мемоизация везде не делает приложение быстрее — она добавляет лишний код и усложняет читаемость.

// Избыточно
const getTitle = useCallback(() => `Привет, ${name}`, [name]);
const isValid = useCallback(() => email.includes('@'), [email]);

// Правильно — простые вычисления не требуют мемоизации
const getTitle = () => `Привет, ${name}`;
const isValid = () => email.includes('@');

Ошибка 4: Отсутствие функционального обновления state

Если функция внутри useCallback читает актуальное значение состояния, но состояние не указано в зависимостях, используйте функциональное обновление.

// НЕПРАВИЛЬНО: count не указан в deps, но читается напрямую
const increment = useCallback(() => {
  setCount(count + 1); // устаревшее значение count!
}, []);

// ПРАВИЛЬНО: функциональное обновление не требует зависимости
const increment = useCallback(() => {
  setCount(prev => prev + 1); // всегда актуально
}, []);

Заключение

useCallback — инструмент оптимизации для конкретных случаев, а не универсальное решение:

  • Применяйте его, когда функция передаётся в React.memo-компоненты или используется как зависимость в useEffect, useMemo, useCallback.
  • Всегда указывайте все используемые значения из области видимости компонента в массиве зависимостей.
  • Используйте функциональное обновление состояния (setCount(prev => prev + 1)), чтобы не добавлять состояние в зависимости.
  • Не применяйте useCallback для простых обработчиков событий, которые никуда не передаются.

Главное правило: сначала пишите простой код, затем измеряйте производительность с помощью React DevTools Profiler и добавляйте useCallback только там, где это реально устраняет проблему.

Стрелочка влевоuseContext — работа с контекстом в ReactuseActionState в React 19Стрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий