Menu

Хук useCallback в React: когда он действительно помогает

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

useCallback кэширует функцию между рендерами: он возвращает один и тот же объект функции, пока не изменится одна из зависимостей. Это важно, только когда что-то сравнивает функцию по ссылке, а на практике это дочерний компонент, обёрнутый в memo, или эффект, который указывает функцию как зависимость.

Обе кнопки мемоизированы, и обе делают одно и то же. Переключите тему и смотрите в Console: появляется только render Inline function, потому что saveInline при каждом рендере новая функция, и memo видит изменившийся пропс. saveCached каждый раз одна и та же функция, поэтому эта кнопка пропускается.

Что он кэширует

const cachedFn = useCallback(fn, dependencies);
  • fn это функция, которую нужно сохранить. React её не вызывает; он возвращает её вам.
  • dependencies перечисляет каждое значение из компонента, которое читает fn. При каждом рендере React сравнивает их с предыдущими через Object.is.
  • Если ничего не изменилось, вы получаете функцию из предыдущего рендера. Если что-то изменилось, вы получаете функцию, переданную в этот раз, и React сохраняет её на следующий раз.

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

useCallback или useMemo

Это один и тот же механизм. useMemo кэширует результат вызова функции, useCallback кэширует саму функцию:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Используйте useCallback для функций, потому что так читается лучше, а useMemo для вычисленных значений, например отфильтрованного списка или объекта настроек.

Как убрать зависимости через функции обновления

Кэшированная функция должна перечислять каждое значение состояния, которое читает, и каждый раз, когда это состояние меняется, меняется и функция. Для списка мемоизированных строк это проблема: функция remove, которая читает todos, меняется при каждом изменении todos, поэтому после каждого удаления все оставшиеся строки рендерятся снова. Используйте форму сеттера с функцией обновления, чтобы функция вообще не читала todos.

Удалите строку: Console молчит, потому что остальные строки получили тот же объект todo и ту же функцию remove, что и раньше. Замените функцию на useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) и попробуйте снова: теперь после каждого удаления каждая оставшаяся строка выводит рендер.

Функции как зависимости эффекта

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

Ввод сообщения не переподключает. Смена комнаты выводит отключение от старой комнаты и подключение к новой. Удалите обёртку useCallback (оставив стрелочную функцию), и каждое нажатие клавиши будет отключать и снова подключать.

Но здесь есть решение проще: перенести функцию внутрь эффекта. Тогда эффект зависит от roomId напрямую, и никакой функции не нужно быть стабильной.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Предпочитайте это, когда функцию использует только эффект. Оставьте useCallback для функций, которые вы ещё и передаёте вниз или возвращаете из пользовательского хука, где вызывающие могут поместить их в собственные массивы зависимостей.

Как проверить, изменилась ли функция

Когда дочерний компонент с memo продолжает рендериться и непонятно почему, проверьте, действительно ли пропс-функция один и тот же объект между рендерами. Храните предыдущую в ref и сравнивайте:

Нажмите Count: Console пишет increment changed: false, потому что step не изменился. Нажмите Step, и он напишет true: step это зависимость, поэтому React вернул новую функцию. Функция, которая меняется при каждом рендере, обычно означает, что какая-то зависимость сама новая при каждом рендере, например объект или другая функция, объявленная в компоненте.

useCallback в пользовательских хуках

Пользовательский хук, который возвращает функции, хорошее место для useCallback, даже когда неизвестно, кто будет его вызывать. Компонент, использующий хук, может передать эти функции дочернему компоненту с memo или указать их в эффекте, а сделать их стабильными снаружи он не может. Обёртка внутри хука по умолчанию даёт каждому вызывающему стабильную ссылку:

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

Частая ошибка: оборачивать всё

useCallback вокруг обработчика, который идёт прямо в <button> или <input>, ничего полезного не делает. React не пропускает никакой работы для элемента DOM из-за того, что его обработчик остался прежним, а компонент, не обёрнутый в memo, всё равно рендерится вместе с родителем.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Каждый лишний useCallback добавляет массив зависимостей, в котором можно ошибиться, а устаревшая зависимость это настоящая ошибка (функция продолжает читать старое состояние). Добавляйте его, когда на принимающей стороне есть дочерний компонент с memo или массив зависимостей, а не по умолчанию.

React Compiler 1.0, если ваша сборка его использует, автоматически мемоизирует функции и значения, и большая часть написанных вручную вызовов useCallback становится не нужна. Без компилятора правила выше по-прежнему действуют, а ручной useCallback всё ещё распространён в существующем коде.

Часто задаваемые вопросы

Что делает useCallback?

Он возвращает один и тот же объект функции при каждом рендере, пока не изменится одна из зависимостей. Он не мешает функции создаваться; он возвращает функцию из более раннего рендера вместо новой.

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

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

Чем useCallback отличается от useMemo?

useCallback(fn, deps) кэширует саму fn. useMemo(() => value, deps) кэширует значение, которое возвращает функция. useCallback(fn, deps) в точности равно useMemo(() => fn, deps).

Оборачивать ли каждую функцию в useCallback?

Нет. Если функция идёт в обычный элемент DOM или в компонент без мемоизации, стабильная ссылка ничего не меняет, а лишние массивы зависимостей делают код сложнее для чтения и проще для ошибок.

Делает ли useCallback компонент быстрее?

Сам по себе нет. Он помогает, только когда что-то ниже сравнивает функцию по ссылке, например memo или массив зависимостей эффекта.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ