Что делает useCallback и зачем он нужен?

Хук useCallback в React используется для мемоизации функции, то есть он позволяет сохранить ссылку на функцию между рендерами, если её зависимости не изменились. Это особенно важно, когда функция передаётся в дочерние компоненты или используется в зависимости других хуков (например, useEffect, useMemo, useImperativeHandle), чтобы избежать ненужных перерендеров или побочных эффектов.

Синтаксис

const memoizedCallback = useCallback(() => {
// функция
}, \[dep1, dep2\]);

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

Проблема, которую решает useCallback

В JavaScript функция — это объект, и каждый раз при создании новой функции в компоненте создаётся новая ссылка:

function MyComponent() {
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button>;
}

Каждый рендер создаёт новую версию handleClick, и если вы передаёте эту функцию в React.memo, useEffect, useMemo или в дочерние компоненты, это может вызвать ненужный ререндер, даже если сама логика функции не изменилась.

Пример с React.memo

const Child = React.memo(({ onClick }) => {
console.log("Render child");
return <button onClick={onClick}>Child Button</button>;
});
function Parent() {
const \[count, setCount\] = useState(0);
const handleClick = () => {
console.log('Clicked');
};
return (
<>
&lt;button onClick={() =&gt; setCount(count + 1)}>Increment&lt;/button&gt;
&lt;Child onClick={handleClick} /&gt;
&lt;/&gt;
);
}

Здесь Child будет перерисовываться каждый раз, даже если handleClick делает одно и то же, потому что при каждом рендере создаётся новая функция и нарушается оптимизация React.memo.

Решение:

const handleClick = useCallback(() => {
console.log('Clicked');
}, \[\]);

Теперь handleClick сохраняет одну и ту же ссылку между рендерами, пока зависимости не изменятся, и Child не будет перерисовываться лишний раз.

Пример с useEffect

useEffect(() => {
fetchData();
}, \[fetchData\]);

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

Решение:

const fetchData = useCallback(() => {
// ...

}, \[/\* зависимости \*/\]);

useEffect(() => {

fetchData();

}, \[fetchData\]);

Отличие от useMemo

  • useCallback(fn, deps) ≈ useMemo(() => fn, deps)

  • useMemo возвращает результат вызова функции

  • useCallback возвращает саму функцию (не вызывая её)

Пример для сравнения:

const memoizedFn = useCallback(() => doSomething(a, b), \[a, b\]);
const result = useMemo(() => doSomething(a, b), \[a, b\]);

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

  1. Функции передаются в React.memo-компоненты
    Чтобы предотвратить ненужные перерендеры.

  2. Функции участвуют в зависимости useEffect или useMemo
    Чтобы избежать лишнего срабатывания эффектов.

  3. Функции используются внутри useImperativeHandle в forwardRef
    Чтобы сохранить стабильность API, предоставляемого через рефы.

  4. Функции в event listeners или subscriptions
    Если нужно добавить/удалить слушатели и ссылки на функции должны быть стабильными.

Потенциальные ловушки

  • Не использовать без необходимости. В большинстве случаев React достаточно умен, чтобы обновлять компоненты эффективно. Чрезмерное использование useCallback может усложнить код и даже снизить производительность, особенно если вы мемоизируете функции с большим количеством зависимостей.

  • Мемоизация стоит ресурсов. Каждый вызов useCallback сам по себе стоит вычислений — React будет сравнивать зависимости, чтобы решить, создавать ли новую функцию. Если хук используется для простой функции без передачи в memo, пользы может не быть вовсе.

  • Следите за зависимостями. Если указать неправильные зависимости, результат может быть неожиданным: компонент будет использовать устаревшие значения (закрытие над старыми значениями переменных).

Пример useCallback в списке

function TodoList({ todos, onToggle }) {
return (
&lt;ul&gt;
{todos.map((todo) => (
&lt;li key={todo.id} onClick={() =&gt; onToggle(todo.id)}>
{todo.text}
&lt;/li&gt;
))}
&lt;/ul&gt;
);
}
const MemoizedTodoList = React.memo(TodoList);
function App() {
const \[todos, setTodos\] = useState(\[\]);
const toggleTodo = useCallback((id) => {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}, \[\]);
return &lt;MemoizedTodoList todos={todos} onToggle={toggleTodo} /&gt;;
}

Без useCallback компонент TodoList перерисовывался бы каждый раз, потому что onToggle — это новая функция при каждом рендере. С useCallback ссылка сохраняется, если todos не изменились.

useCallback и производительность

Мемоизация с useCallback особенно важна, когда:

  • есть много компонентов в дереве

  • много объектов/функций передаются по props

  • используются дорогостоящие операции, которые зависят от тех же самых функций

Для измерения производительности можно использовать React Profiler или инструмент why-did-you-render, чтобы понять, когда компонент перерисовывается и почему.

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