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

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#React #Hooks

Краткий ответ

useCallback и useMemo — это React хуки для оптимизации производительности через мемоизацию:

  • useCallback мемоизирует функцию
  • useMemo мемоизирует результат вычисления

Простой пример различий:

// useCallback возвращает мемоизированную функцию
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);
 
// useMemo возвращает мемоизированное значение
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);

Полный ответ

Оба хука решают проблему лишних перерендеров и пересчётов в React-компонентах, но применяются в разных ситуациях.

useCallback — мемоизация функций

Что делает

Возвращает мемоизированную версию колбэк-функции, которая изменяется только при изменении зависимостей:

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} />;
}

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

  1. Передача функций в мемоизированные компоненты:
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>
    </>
  );
}
  1. Зависимость в других хуках:
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)} />;
}

useMemo — мемоизация значений

Что делает

Возвращает мемоизированное значение, пересчитывая его только при изменении зависимостей:

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>;
}

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

  1. Дорогие вычисления:
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>
  );
}
  1. Референциальное равенство объектов:
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>;
}

Ключевые различия

1. Тип возвращаемого значения

// 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"

2. Эквивалентность

// 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>
  );
});

Частые ошибки

1. Избыточная оптимизация

// ❌ Плохо: простые вычисления не требуют мемоизации
const sum = useMemo(() => a + b, [a, b]);
 
// ✅ Хорошо: просто вычисляем
const sum = a + b;

2. Неправильные зависимости

// ❌ Плохо: отсутствует зависимость
const calculate = useCallback(() => {
  return data.length * multiplier; // multiplier не в зависимостях
}, [data]);
 
// ✅ Хорошо: все внешние переменные указаны
const calculate = useCallback(() => {
  return data.length * multiplier;
}, [data, multiplier]);

3. Мемоизация примитивов

// ❌ Бессмысленно: строки и числа и так сравниваются по значению
const name = useMemo(() => 'John', []);
 
// ✅ Имеет смысл: объекты сравниваются по ссылке
const config = useMemo(() => ({ name: 'John' }), []);

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

useCallback подходит для:

  • Передачи функций в мемоизированные компоненты
  • Функций-зависимостей в useEffect, useMemo
  • Предотвращения пересоздания обработчиков событий

useMemo подходит для:

  • Дорогих вычислений (сортировка, фильтрация больших массивов)
  • Создания объектов/массивов, используемых как зависимости
  • Оптимизации рендеринга дочерних компонентов

Не используйте когда:

  • Вычисления простые и быстрые
  • Компонент редко перерендеривается
  • Нет проблем с производительностью

Лучшие практики

  1. Измеряйте производительность перед оптимизацией
  2. Начинайте без мемоизации, добавляйте при необходимости
  3. Используйте React DevTools Profiler для поиска узких мест
  4. Помните о затратах на саму мемоизацию
  5. Правильно указывайте зависимости

Помните: преждевременная оптимизация — корень всех зол. Используйте эти хуки только когда они действительно решают проблемы производительности.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪