useCallback полезен не сам по себе, а в конкретной ситуации: когда useEffect зависит от функции, а функция создается заново на каждом рендере. Пока не понять эту причинно-следственную связь, useCallback легко превращается в бессмысленную обертку.

Проблема выглядит так:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Но важен не синтаксис, а вопрос: почему вообще понадобилось стабилизировать функцию.

Где берется лишний запуск useEffect

Каждый рендер функционального компонента заново выполняет его тело. Значит, если ты объявляешь функцию внутри компонента, то получаешь новую ссылку на эту функцию на каждом рендере.

function SomeComponent() {
  function getUrl(id) {
    return `https://some-api-url.com/api/${id}`;
  }

  return <DataFetcher getUrl={getUrl} />;
}

Если дочерний компонент использует getUrl в зависимостях useEffect, React видит новую функцию и честно перезапускает эффект:

function DataFetcher({ getUrl, id }) {
  useEffect(() => {
    fetchDataToDoSomething(getUrl(id));
  }, [getUrl, id]);

  return null;
}

Сам useEffect здесь работает правильно. Проблема в нестабильной ссылке getUrl.

Когда useCallback действительно помогает

Если функция действительно должна жить внутри компонента и передаваться дальше как зависимость, ее можно стабилизировать через useCallback.

import { useCallback } from "react";

function SomeComponent() {
  const getUrl = useCallback((id) => {
    return `https://some-api-url.com/api/${id}`;
  }, []);

  return <DataFetcher id={42} getUrl={getUrl} />;
}

Теперь ссылка на getUrl не меняется между рендерами, пока не изменятся зависимости самого useCallback.

Важно: useCallback не “запрещает ререндеры” и не “ускоряет все подряд”. Он лишь возвращает ту же функцию между рендерами, если зависимости не поменялись.

Когда лучше обойтись без useCallback

Часто лучший вариант вообще не в useCallback, а в том, чтобы убрать лишнюю зависимость.

Если функция нужна только внутри эффекта, проще определить ее прямо там:

function DataFetcher({ id }) {
  useEffect(() => {
    function getUrl() {
      return `https://some-api-url.com/api/${id}`;
    }

    fetchDataToDoSomething(getUrl());
  }, [id]);

  return null;
}

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

function getUrl(id) {
  return `https://some-api-url.com/api/${id}`;
}

function SomeComponent() {
  return <DataFetcher id={42} getUrl={getUrl} />;
}

Это обычно проще, чем оборачивать все подряд в useCallback.

Итог

Лишний запуск useEffect появляется не потому, что React “ведет себя странно”, а потому, что новая функция на каждом рендере считается новой зависимостью. Сначала проверь, можно ли убрать эту функцию из зависимостей совсем. Если нельзя, тогда useCallback действительно уместен.