Зачем нужен useCallback?
Когда вы разрабатываете React-компоненты, одна из распространённых проблем — случайная переинициализация функций. Каждый раз, когда компонент рендерится, все функции внутри него создаются заново. В большинстве случаев это не вызывает проблем. Но если функция передаётся как пропс дочернему компоненту (особенно мемоизированному с помощью React.memo), она может заставить этот дочерний компонент перерендериваться, даже если его пропсы на самом деле не изменились.
Пример проблемы:
import React, { useState } from 'react';
interface ButtonProps {
onClick: () => void;
}
const Button: React.FC<ButtonProps> = React.memo(({ onClick }) => {
console.log('Button re-rendered');
return <button onClick={onClick}>Click me</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Button clicked');
setCount(count + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<Button onClick={handleClick} />
</div>
);
};
Проблема: даже если мы обернули Button в React.memo, каждый раз, когда рендерится App, функция handleClick создаётся заново. Из-за этого Button перерендерится, несмотря на мемоизацию. Зачем тогда нам вообще React.memo, если мемоизация теряет смысл?
Что делает useCallback?
useCallback помогает нам мемоизировать функцию, чтобы она оставалась неизменной между рендерами, если её зависимости не изменились.
Синтаксис:
const memoizedCallback = useCallback(
() => {
// Ваш код
},
[dependencies]
);
useCallbackвозвращает ту же самую функцию между рендерами, если зависимости в массиве[dependencies]остаются неизменными.- Если зависимости изменились,
useCallbackсоздаёт новую функцию.
Давайте исправим наш предыдущий пример с помощью useCallback.
Исправленный пример:
import React, { useState, useCallback } from 'react';
interface ButtonProps {
onClick: () => void;
}
const Button: React.FC<ButtonProps> = React.memo(({ onClick }) => {
console.log('Button re-rendered');
return <button onClick={onClick}>Click me</button>;
});
const App = () => {
const [count, setCount] = useState(0);
// Оборачиваем handleClick в useCallback
const handleClick = useCallback(() => {
console.log('Button clicked');
setCount((prev) => prev + 1);
}, []); // handleClick не зависит ни от каких переменных, поэтому зависимости пустые
return (
<div>
<h1>Count: {count}</h1>
<Button onClick={handleClick} />
</div>
);
};
Теперь handleClick будет оставаться неизменным между рендерами App, и Button больше не будет ненужных перерендеров.
Когда useCallback полезен?
Представьте себе, что ваш компонент передаёт функции вниз по дереву компонентам, которые в свою очередь мемоизированы с помощью React.memo. Если эти функции создаются заново на каждом рендере, мемоизация дочерних компонентов теряет смысл.
Основные случаи:
- Мемоизация функций, передаваемых в
React.memo-компоненты. - Передача функций в зависимости
useEffectдругих компонентов. - Оптимизация сложной логики (например, слушателей событий).
На что обратить внимание?
Ну ладно, useCallback звучит круто, но как и любая сверхъестественная сила в программировании, он имеет свои недостатки.
Избыточное использование useCallback
Мы не всегда должны бросаться мемоизировать всё подряд. Оптимизация ради оптимизации может сделать код излишне сложным и, что ещё хуже, уменьшить производительность. В некоторых случаях проще позволить функции создаваться заново без useCallback. Например: если функция очень простая (2+2=4, знаете ли) или почти не вызывается.
Зависимости useCallback
Самая большая ошибка, которую делают разработчики при работе с useCallback, — это неправильное указание зависимостей. Если вы забудете какую-либо зависимость, ваша функция может начать работать с устаревшими данными.
Давайте рассмотрим пример:
const App = () => {
const [count, setCount] = useState(0);
// Ошибка: зависимости не указаны
const handleClick = useCallback(() => {
console.log(`Count is: ${count}`);
setCount(count + 1);
}, []); // Здесь count должен быть зависимостью!
return (
<button onClick={handleClick}>Click me</button>
);
};
Исправление:
const handleClick = useCallback(() => {
console.log(`Count is: ${count}`);
setCount(count + 1);
}, [count]); // Добавляем count в зависимости
Если указать зависимости неправильно, вы можете столкнуться с багами, трудными для отладки.
Типизация функций в useCallback
Как же типизировать функцию, которую возвращает useCallback, с TypeScript? Очень просто! Вы просто задаёте типы для параметров и возвращаемого значения функции.
Пример:
import React, { useCallback } from 'react';
// Определяем тип компонента
interface ButtonProps {
onClick: (value: string) => void;
}
const Button: React.FC<ButtonProps> = ({ onClick }) => {
return <button onClick={() => onClick('Hello')}>Click me</button>;
};
const App = () => {
const handleClick = useCallback((value: string) => {
console.log('Clicked with value:', value);
}, []);
return <Button onClick={handleClick} />;
};
Объявление типов для useCallback:
Если вы хотите быть ещё более явным, вы можете использовать интерфейс для типов аргументов функции.
interface CallbackFunction {
(value: string): void;
}
const handleClick: CallbackFunction = useCallback((value: string) => {
console.log('Clicked with value:', value);
}, []);
Практические примеры
Давайте рассмотрим, как useCallback помогает в реальной разработке.
Пример: список с кнопками действий
Представьте, что у нас есть список задач, и вы хотите предоставить пользователю возможность удалять задачи.
import React, { useState, useCallback } from 'react';
interface Task {
id: number;
name: string;
}
const TaskList: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, name: 'Learn React' },
{ id: 2, name: 'Learn TypeScript' },
{ id: 3, name: 'Build a project' },
]);
// Мемоизируем функцию для удаления задачи
const handleDelete = useCallback((id: number) => {
setTasks((prevTasks) => prevTasks.filter((task) => task.id !== id));
}, []);
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.name}
<button onClick={() => handleDelete(task.id)}>Delete</button>
</li>
))}
</ul>
);
};
Здесь handleDelete остаётся неизменной между рендерами благодаря useCallback. Это особенно полезно, если у вас 100+ задач — вы не хотите, чтобы кнопки для каждой задачи пересоздавались.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ