Комментарии (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 под капотом работает так:
- Fiber архитектура: React хранит каждый компонент в Fiber объекте с связанным списком hooks
- Сохранение состояния: При первом вызове useCallback сохраняет функцию и зависимости
- Сравнение dependencies: При следующем рендере сравнивает новые зависимости со старыми используя Object.is
- Переиспользование функции: Если зависимости не изменились, возвращает сохранённую функцию (та же ссылка в памяти)
- Обновление: Если зависимости изменились, создаёт новую функцию
Это позволяет избежать создания новых функций при каждом рендере, что критично для:
- React.memo оптимизации (сравнение пропсов)
- Зависимостей других hooks (useEffect)
Но нужно помнить: useCallback сам занимает время. Его имеет смысл использовать только если callback передаётся React.memo компоненту или используется в зависимостях других hooks. Для простых случаев лучше просто не использовать.»