← Назад к вопросам

Как useCallback помогает оптимизировать дочерние компоненты?

2.0 Middle🔥 221 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как useCallback помогает оптимизировать дочерние компоненты?

useCallback — это хук React, который мемоизирует функции, предотвращая их пересоздание на каждом рендере. Это критически важно для оптимизации производительности при передаче функций в дочерние компоненты, обёрнутые в React.memo.

Проблема без useCallback

Когда вы передаёте функцию в дочерний компонент, она пересоздаётся на каждом рендере родителя:

const Parent = () => {
  const [count, setCount] = useState(0);

  // Эта функция пересоздаётся на КАЖДОМ рендере
  const handleClick = () => {
    console.log('Clicked');
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child onClick={handleClick} /> {/* Каждый раз передаём ДРУ ГУЮ функцию */}
    </div>
  );
};

const Child = React.memo(({ onClick }) => {
  console.log('Child rendering');
  return <button onClick={onClick}>Click me</button>;
});

// Результат:
// При нажатии на кнопку count меняется
// Parent перерендерится
// handleClick пересоздаётся (новая функция с новой ссылкой)
// React.memo видит: prevProps.onClick !== nextProps.onClick
// Child перерендерится БЕЗ необходимости!

Решение: useCallback

useCallback мемоизирует функцию, пока её зависимости не изменятся:

const Parent = () => {
  const [count, setCount] = useState(0);

  // Функция пересоздаётся ТОЛЬКО если 'count' изменится
  const handleClick = useCallback(() => {
    console.log('Clicked with count:', count);
  }, [count]); // Зависимости

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child onClick={handleClick} /> {/* Передаём стабильную функцию */}
    </div>
  );
};

const Child = React.memo(({ onClick }) => {
  console.log('Child rendering');
  return <button onClick={onClick}>Click me</button>;
});

// Результат:
// При нажатии на кнопку count меняется
// Parent перерендерится
// useCallback видит: count изменился
// Функция пересоздаётся
// React.memo видит: prevProps.onClick !== nextProps.onClick
// Child перерендерится (есть причина)

Практический пример: Список с добавлением элементов

const TodoApp = () => {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');

  // НЕПРАВИЛЬНО - без useCallback
  const addTodoWrong = () => {
    setTodos([...todos, { id: Date.now(), title: inputValue }]);
    setInputValue('');
  };

  // ПРАВИЛЬНО - с useCallback
  const addTodo = useCallback(() => {
    setTodos(prev => [...prev, { id: Date.now(), title: inputValue }]);
    setInputValue('');
  }, [inputValue]); // Зависит от inputValue

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Add todo"
      />
      <AddButton onClick={addTodo} /> {/* Дочерний компонент */}
      <TodoList todos={todos} />
    </div>
  );
};

// React.memo защищает от ненужных рендеров
const AddButton = React.memo(({ onClick }) => {
  console.log('AddButton rendering');
  return <button onClick={onClick}>Add</button>;
});

const TodoList = React.memo(({ todos }) => {
  console.log('TodoList rendering');
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
});

Сравнение: с useCallback vs без

const Parent = () => {
  const [parentCount, setParentCount] = useState(0);
  const [childState, setChildState] = useState(0);

  // ВАРИАНТ 1: БЕЗ useCallback
  const handleChildClick = () => {
    setChildState(childState + 1);
  };

  // ВАРИАНТ 2: С useCallback
  const handleChildClickOptimized = useCallback(() => {
    setChildState(prev => prev + 1);
  }, []); // Зависимостей нет!

  return (
    <div>
      <p>Parent count: {parentCount}</p>
      <button onClick={() => setParentCount(parentCount + 1)}>
        Increment parent (triggers parent rerender)
      </button>
      {/* 
        БЕЗ useCallback:
        Каждый раз, когда parentCount меняется,
        Parent перерендерится и пересоздаст handleChildClick
        Child перерендерится даже если изменилась только родительское состояние
      */}
      <Child onClick={handleChildClick} />
      
      {/* 
        С useCallback и пустыми зависимостями:
        handleChildClickOptimized НИКОГДА не пересоздаётся
        Child рендерится ТОЛЬКО если childState меняется
      */}
      <Child onClick={handleChildClickOptimized} />
    </div>
  );
};

const Child = React.memo(({ onClick }) => {
  console.log('Child rendering');
  return <button onClick={onClick}>Click child</button>;
});

useCallback с правильными зависимостями

const UserManager = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // Функция зависит от userId
  const fetchUser = useCallback(
    async (userId) => {
      setLoading(true);
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
      setLoading(false);
    },
    [] // Нет зависимостей (параметр передаётся явно)
  );

  // Функция зависит от user объекта
  const deleteUser = useCallback(() => {
    if (user) {
      fetch(`/api/users/${user.id}`, { method: 'DELETE' });
      setUser(null);
    }
  }, [user]); // Зависит от user

  // Функция с несколькими зависимостями
  const updateUser = useCallback(
    (updates) => {
      setUser(prev => ({ ...prev, ...updates }));
    },
    [] // Обновление состояния, зависимостей нет
  );

  return (
    <div>
      <UserProfile user={user} onFetch={fetchUser} onUpdate={updateUser} />
      <button onClick={() => deleteUser()}>Delete</button>
      {loading && <p>Loading...</p>}
    </div>
  );
};

const UserProfile = React.memo(({ user, onFetch, onUpdate }) => {
  useEffect(() => {
    if (user) {
      onFetch(user.id);
    }
  }, [user.id, onFetch]); // onFetch стабильна благодаря useCallback

  return (
    <div>
      {user && <h2>{user.name}</h2>}
      <button onClick={() => onUpdate({ name: 'Updated' })}>Update</button>
    </div>
  );
});

Когда НЕ нужен useCallback

const Component = () => {
  // НЕ нужен useCallback для простых случаев
  const handleClick = () => console.log('clicked');

  return <button onClick={handleClick}>Click</button>;
};

// useCallback имеет смысл ТОЛЬКО если:
// 1. Функция передаётся дочернему компоненту
// 2. Дочерний компонент обёрнут в React.memo
// 3. Функция используется в зависимостях других хуков (useEffect, useMemo)

Проблемы неправильного использования useCallback

// ПРОБЛЕМА 1: Лишние зависимости
const wrong = useCallback(() => {
  console.log(data1, data2, data3); // 3 объекта
}, [data1, data2, data3]); // Функция пересоздаётся часто

// ЛУЧШЕ: Использовать функциональное обновление
const better = useCallback(() => {
  setData(prev => ({ ...prev, field: newValue }));
}, []); // Нет зависимостей

// ПРОБЛЕМА 2: Неправильные зависимости
const wrongDeps = useCallback(
  (userId) => fetch(`/api/users/${userId}`),
  [] // Забыли apiUrl если он переменный
);

// ПРОБЛЕМА 3: useCallback на всё подряд
const Component = () => {
  // Не нужен useCallback для простых функций,
  // которые не передаются в дочерние компоненты
  const simple = useCallback(() => {}, []); // Пустая трата памяти
};

Итог

useCallback помогает оптимизировать дочерние компоненты:

  1. Предотвращает пересоздание функций — одна и та же ссылка на функцию
  2. Работает с React.memo — дочерний компонент видит, что props не изменился
  3. Стабильные зависимости для useEffect — избегаем бесконечных циклов
  4. Значит только с дочерними компонентами — не используй везде
  5. Требует правильных зависимостей — иначе код будет неправильным

Используй useCallback стратегически: только для функций, передаваемых в React.memo компоненты или используемых в зависимостях других хуков.

Как useCallback помогает оптимизировать дочерние компоненты? | PrepBro