Что делает 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 (
<>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
</>
);
}
Здесь 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
-
Функции передаются в React.memo-компоненты
Чтобы предотвратить ненужные перерендеры. -
Функции участвуют в зависимости useEffect или useMemo
Чтобы избежать лишнего срабатывания эффектов. -
Функции используются внутри useImperativeHandle в forwardRef
Чтобы сохранить стабильность API, предоставляемого через рефы. -
Функции в event listeners или subscriptions
Если нужно добавить/удалить слушатели и ссылки на функции должны быть стабильными.
Потенциальные ловушки
-
Не использовать без необходимости. В большинстве случаев React достаточно умен, чтобы обновлять компоненты эффективно. Чрезмерное использование useCallback может усложнить код и даже снизить производительность, особенно если вы мемоизируете функции с большим количеством зависимостей.
-
Мемоизация стоит ресурсов. Каждый вызов useCallback сам по себе стоит вычислений — React будет сравнивать зависимости, чтобы решить, создавать ли новую функцию. Если хук используется для простой функции без передачи в memo, пользы может не быть вовсе.
-
Следите за зависимостями. Если указать неправильные зависимости, результат может быть неожиданным: компонент будет использовать устаревшие значения (закрытие над старыми значениями переменных).
Пример useCallback в списке
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}
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 <MemoizedTodoList todos={todos} onToggle={toggleTodo} />;
}
Без useCallback компонент TodoList перерисовывался бы каждый раз, потому что onToggle — это новая функция при каждом рендере. С useCallback ссылка сохраняется, если todos не изменились.
useCallback и производительность
Мемоизация с useCallback особенно важна, когда:
-
есть много компонентов в дереве
-
много объектов/функций передаются по props
-
используются дорогостоящие операции, которые зависят от тех же самых функций
Для измерения производительности можно использовать React Profiler или инструмент why-did-you-render, чтобы понять, когда компонент перерисовывается и почему.
Хук useCallback даёт контроль над стабильностью ссылок на функции, предотвращает лишние рендеры, помогает в сложных структурах компонентов и повышает производительность, когда используется правильно.