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

Как реализован useCallback под капотом?

2.2 Middle🔥 122 комментариев
#React

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

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

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

Как реализован useCallback под капотом

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

1. Простое объяснение

// БЕЗ useCallback - функция создаётся заново
function Parent() {
  const [count, setCount] = React.useState(0);
  
  // Каждый раз при рендере создаётся НОВАЯ функция
  const handleClick = () => {
    console.log('Clicked');
  };
  
  // React.memo не поможет, потому что handleClick разные
  return <Child onClick={handleClick} />;
}

// С useCallback - функция переиспользуется
function Parent() {
  const [count, setCount] = React.useState(0);
  
  // Эта функция создаётся один раз, переиспользуется
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);
  
  // Теперь Child всегда получает одну и ту же функцию
  return <Child onClick={handleClick} />;
}

2. Механизм работы: Fiber и Hooks State

// React сохраняет информацию о каждом компоненте в Fiber объект
// Для hooks используется связанный список (linked list)

// Упрощённая реализация
let componentHooks = []; // Связанный список для каждого компонента
let hookIndex = 0; // Текущий индекс при исполнении

function useCallback(callback, dependencies) {
  // Получаем хранилище для этого хука
  // React знает какой компонент сейчас исполняется
  const hook = componentHooks[hookIndex];
  
  if (!hook) {
    // Первый раз - сохраняем callback и dependencies
    componentHooks[hookIndex] = {
      callback,      // Сохраняем функцию
      dependencies,  // Сохраняем dependencies массив
    };
  } else {
    // При повторном рендере - сравниваем dependencies
    const dependenciesChanged = hasDependenciesChanged(
      hook.dependencies,
      dependencies
    );
    
    if (dependenciesChanged) {
      // Dependencies изменились - обновляем callback
      hook.callback = callback;
      hook.dependencies = dependencies;
    }
    // Иначе возвращаем старую функцию
  }
  
  hookIndex++;
  return hook.callback; // Возвращаем сохранённую функцию
}

3. Сравнение dependencies

// Как React сравнивает dependencies массивы
function hasDependenciesChanged(prevDeps, nextDeps) {
  if (prevDeps === undefined) {
    return true; // Первый раз
  }
  
  if (prevDeps.length !== nextDeps.length) {
    return true; // Разная длина
  }
  
  // Поэлементное сравнение (Object.is)
  for (let i = 0; i < prevDeps.length; i++) {
    if (!Object.is(prevDeps[i], nextDeps[i])) {
      return true; // Какая-то зависимость изменилась
    }
  }
  
  return false; // Ничего не изменилось
}

// Object.is примерно равно ===
// Object.is(1, 1) -> true
// Object.is({}, {}) -> false (разные объекты)
// Object.is(NaN, NaN) -> true (в отличие от ===)

4. Реальный пример с логикой

function SearchUsers() {
  const [query, setQuery] = React.useState('');
  const [results, setResults] = React.useState([]);
  const [page, setPage] = React.useState(1);
  
  // При каждом рендере это создаёт НОВУЮ функцию
  const fetchResults = () => {
    fetch(`/api/search?q=${query}&page=${page}`)
      .then(res => res.json())
      .then(data => setResults(data));
  };
  
  // Callback зависит от query и page
  const memoizedFetch = useCallback(() => {
    fetch(`/api/search?q=${query}&page=${page}`)
      .then(res => res.json())
      .then(data => setResults(data));
  }, [query, page]);
  // Эта функция пересоздаётся ТОЛЬКО если query или page изменились
  
  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)}
      />
      <button onClick={memoizedFetch}>Search</button>
      <UserResults results={results} />
    </div>
  );
}

// Без useCallback: каждый рендер -> новая fetchResults -> UserResults пересчитывается
// С useCallback: только если query/page изменились -> новая fetchResults

5. Внутренний механизм Fiber

// Реальная структура React Fiber
interface Fiber {
  type: any;           // Тип компонента (функция, класс)
  key: string | null;  // Key для элемента
  memoizedState: any;  // Связанный список hooks
  component: Function; // Сама функция компонента
  // ... много других полей
}

interface Hook {
  memoizedState: any;  // Сохранённое значение (для useCallback это функция)
  baseState: any;
  queue: any;
  next: Hook | null;   // Следующий хук в списке
}

// Когда React выполняет компонент:
// 1. Устанавливает currentFiber = fiber этого компонента
// 2. Устанавливает hookIndex = 0
// 3. Вызывает функцию компонента
// 4. При каждом useCallback():
//    - Получает hook из fiber.memoizedState[hookIndex]
//    - Сравнивает dependencies
//    - Возвращает сохранённую или новую функцию
//    - hookIndex++
// 5. После выполнения hookIndex = 0 (для следующего рендера)

6. Правила использования useCallback

// ✅ Правильно: useCallback с зависимостями
const handleClick = useCallback(() => {
  setCount(count + 1);
}, [count]); // count - это зависимость

// ✅ Лучше: использовать функциональное обновление
const handleClick = useCallback(() => {
  setCount(c => c + 1);
}, []); // Без зависимостей!

// ❌ Неправильно: забыли зависимость
const handleClick = useCallback(() => {
  setCount(count + 1);
}, []); // count не в зависимостях, но используется!
// Это будет ошибка!

// ❌ Неправильно: используем все переменные как зависимости
const handleClick = useCallback(() => {
  console.log(query, page, sorting, filters);
}, [query, page, sorting, filters]);
// Callback пересоздаётся при каждом изменении, смысла нет
// Лучше просто не использовать useCallback

7. Когда useCallback действительно нужен

// Сценарий 1: Callback передаётся React.memo компоненту
const Child = React.memo(({ onClick }) => {
  console.log('Child render');
  return <button onClick={onClick}>Click</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // БЕЗ useCallback - Child рендерится при каждом клике Parent
  const handleClick = () => setCount(c => c + 1);
  
  // С useCallback - Child НЕ рендерится если dependencies не изменились
  const memoizedClick = useCallback(() => setCount(c => c + 1), []);
  
  return <Child onClick={memoizedClick} />;
}

// Сценарий 2: Callback используется в зависимостях другого hook
function Component() {
  const fetchData = useCallback(() => {
    fetch('/api/data');
  }, []);
  
  // fetchData теперь стабильная, не пересоздаётся
  useEffect(() => {
    fetchData();
  }, [fetchData]); // Безопасно!
}

// Сценарий 3: Callback выполняет дорогую операцию
const handleComplexCalculation = useCallback(() => {
  // Сложный расчёт
  const result = complexMath(data);
  return result;
}, [data]);

8. Оптимизация useCallback

// Антипаттерн: useCallback с большим количеством зависимостей
const handleSubmit = useCallback(() => {
  // Использует query, page, sort, filter, theme
  submitForm({ query, page, sort, filter, theme });
}, [query, page, sort, filter, theme]);
// Пересоздаётся всё равно при каждом изменении

// Лучше: использовать useRef для изменяемых значений
const queryRef = useRef(query);
const pageRef = useRef(page);

const handleSubmit = useCallback(() => {
  submitForm({
    query: queryRef.current,
    page: pageRef.current,
  });
}, []);

useEffect(() => {
  queryRef.current = query;
}, [query]);

useEffect(() => {
  pageRef.current = page;
}, [page]);

// Или использовать useReducer для связанного state
const [state, dispatch] = useReducer(reducer, initialState);
const handleSubmit = useCallback(() => {
  submitForm(state);
}, [state]);

9. Производительность useCallback

// Измерение эффективности
function Parent() {
  const [count, setCount] = useState(0);
  console.time('render');
  
  // Без useCallback
  const handleClick = () => setCount(c => c + 1);
  
  // С useCallback
  const memoizedClick = useCallback(
    () => setCount(c => c + 1),
    []
  );
  
  console.timeEnd('render');
  
  // useCallback сам занимает время
  // Его имеет смысл использовать только если он спасает
  // re-renders дорогих компонентов-детей
}

10. Альтернативы useCallback

// 1. Просто не использовать, если не нужно
function Parent() {
  return <Child onClick={() => console.log('click')} />;
}

// 2. Функциональное обновление без useCallback
const setCount = useCallback(
  (newCount) => setState(newCount),
  []
);

// 3. Передавать зависимости в Child через props
function Parent() {
  const [count, setCount] = useState(0);
  return <Child count={count} />; // Передаём данные, не функцию
}

Ответ на интервью

«useCallback под капотом работает так:

  1. Fiber архитектура: React хранит каждый компонент в Fiber объекте с связанным списком hooks
  2. Сохранение состояния: При первом вызове useCallback сохраняет функцию и зависимости
  3. Сравнение dependencies: При следующем рендере сравнивает новые зависимости со старыми используя Object.is
  4. Переиспользование функции: Если зависимости не изменились, возвращает сохранённую функцию (та же ссылка в памяти)
  5. Обновление: Если зависимости изменились, создаёт новую функцию

Это позволяет избежать создания новых функций при каждом рендере, что критично для:

  • React.memo оптимизации (сравнение пропсов)
  • Зависимостей других hooks (useEffect)

Но нужно помнить: useCallback сам занимает время. Его имеет смысл использовать только если callback передаётся React.memo компоненту или используется в зависимостях других hooks. Для простых случаев лучше просто не использовать.»

Как реализован useCallback под капотом? | PrepBro