useCallback и useMemo — это React хуки для оптимизации производительности через мемоизацию:
Простой пример различий:
// useCallback возвращает мемоизированную функцию
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);
// useMemo возвращает мемоизированное значение
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);Оба хука решают проблему лишних перерендеров и пересчётов в React-компонентах, но применяются в разных ситуациях.
Возвращает мемоизированную версию колбэк-функции, которая изменяется только при изменении зависимостей:
function ParentComponent() {
const [count, setCount] = useState(0);
// Без useCallback: новая функция при каждом рендере
const handleClick = () => {
console.log(count);
};
// С useCallback: та же функция, пока count не изменится
const memoizedHandleClick = useCallback(() => {
console.log(count);
}, [count]);
return <ChildComponent onClick={memoizedHandleClick} />;
}const ExpensiveChild = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Без useCallback Child будет перерендериваться
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Пустые зависимости = функция создаётся один раз
return (
<>
<ExpensiveChild onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</>
);
}function SearchComponent() {
const [query, setQuery] = useState('');
const search = useCallback((searchQuery) => {
// Логика поиска
console.log('Searching for:', searchQuery);
}, []); // Функция не пересоздаётся
// useEffect не будет срабатывать при каждом рендере
useEffect(() => {
if (query) {
search(query);
}
}, [query, search]);
return <input onChange={(e) => setQuery(e.target.value)} />;
}Возвращает мемоизированное значение, пересчитывая его только при изменении зависимостей:
function ExpensiveComponent({ items }) {
// Без useMemo: пересчёт при каждом рендере
const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);
// С useMemo: пересчёт только при изменении items
const memoizedValue = useMemo(
() => items.reduce((sum, item) => sum + item.value, 0),
[items]
);
return <div>Total: {memoizedValue}</div>;
}function DataTable({ data, filter }) {
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]);
return (
<table>
{filteredData.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</table>
);
}function UserProfile({ userId }) {
// Новый объект при каждом рендере может вызвать лишние эффекты
const userConfig = useMemo(() => ({
id: userId,
theme: 'dark',
permissions: ['read', 'write']
}), [userId]);
// useEffect сработает только при изменении userId
useEffect(() => {
loadUserData(userConfig);
}, [userConfig]);
return <div>User: {userId}</div>;
}// useCallback возвращает функцию
const memoizedFn = useCallback(() => {
return a + b;
}, [a, b]);
// useMemo возвращает результат выполнения функции
const memoizedResult = useMemo(() => {
return a + b;
}, [a, b]);
console.log(typeof memoizedFn); // "function"
console.log(typeof memoizedResult); // "number"// useCallback
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b]
);
// Эквивалентно useMemo, возвращающему функцию
const memoizedCallback = useMemo(
() => () => doSomething(a, b),
[a, b]
);function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
// Мемоизация обработчика
const handleChange = useCallback((field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
}, []); // Функция создаётся один раз
// Мемоизация валидации
const isValid = useMemo(() => {
return formData.name.length > 0 &&
formData.email.includes('@') &&
formData.message.length > 10;
}, [formData]);
return (
<form>
<input
value={formData.name}
onChange={handleChange('name')}
/>
<input
value={formData.email}
onChange={handleChange('email')}
/>
<textarea
value={formData.message}
onChange={handleChange('message')}
/>
<button disabled={!isValid}>
Отправить
</button>
</form>
);
}function TodoList({ todos, filter }) {
// Мемоизация фильтрации
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
// Мемоизация обработчика
const toggleTodo = useCallback((id) => {
// Логика изменения todo
}, []);
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
/>
))}
</ul>
);
}
const TodoItem = React.memo(({ todo, onToggle }) => {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});// ❌ Плохо: простые вычисления не требуют мемоизации
const sum = useMemo(() => a + b, [a, b]);
// ✅ Хорошо: просто вычисляем
const sum = a + b;// ❌ Плохо: отсутствует зависимость
const calculate = useCallback(() => {
return data.length * multiplier; // multiplier не в зависимостях
}, [data]);
// ✅ Хорошо: все внешние переменные указаны
const calculate = useCallback(() => {
return data.length * multiplier;
}, [data, multiplier]);// ❌ Бессмысленно: строки и числа и так сравниваются по значению
const name = useMemo(() => 'John', []);
// ✅ Имеет смысл: объекты сравниваются по ссылке
const config = useMemo(() => ({ name: 'John' }), []);✅ useCallback подходит для:
✅ useMemo подходит для:
❌ Не используйте когда:
Помните: преждевременная оптимизация — корень всех зол. Используйте эти хуки только когда они действительно решают проблемы производительности.
Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪