useCallback — це хук React, який повертає мемоізовану версію функції, що не змінюється між рендерами, доки не зміняться її залежності. На відміну від звичайного оголошення функції всередині компонента (яке створює нову функцію при кожному рендері), useCallback стабілізує посилання на функцію, що запобігає непотрібним ререндерам дочірніх компонентів, оптимізованих через React.memo. За даними React Documentation (2025), useCallback корисний тільки в парі з React.memo або хуками, що залежать від стабільного посилання.
Головне
useCallback — це хук, доданий у React 16.8, який мемоізує функцію: повертає одне й те саме посилання, доки залежності не зміняться. Без useCallback кожне оголошення функції всередині компонента створює новий об'єкт функції при кожному рендері. Для примітивів це непомітно, але при передачі таких колбеків у дочірні компоненти, оптимізовані через React.memo, кожне нове посилання викликає ререндер дочірнього компонента.
Синтаксично useCallback еквівалентний useMemo для функції: useCallback(fn, deps) — це скорочення для useMemo(() => fn, deps). React зберігає мемоізовану функцію у внутрішньому сховищі fiber-вузла і порівнює залежності при кожному рендері. Якщо залежності не змінилися (Object.is для кожного елемента), повертається попередня функція.
За даними React Documentation — useCallback (2025), не варто обгортати кожну функцію в useCallback. Хук має свою ціну: виклик хука, порівняння залежностей і алокація пам'яті під масив залежностей. Якщо компонент простий і не має глибоких дерев з React.memo — useCallback буде тільки сповільнювати додаток. Оптимізація повинна бути вимірюваною, а не інтуїтивною.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Стабільне посилання — та ж функція, поки deps не зміняться
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
Мемоізація в useCallback заснована на кешуванні результату виклику функції. React зберігає замикання, створене в момент першого рендера, і повертає його при наступних рендерах, поки залежності залишаються незмінними. Всередині fiber-вузла кожен виклик useCallback створює вузол у зв'язаному списку хуків, де зберігаються попередні залежності та мемоізоване значення.
Процес порівняння залежностей виконується строго через Object.is — це поверхневе порівняння без глибокої перевірки об'єктів або масивів. Якщо в залежностях об'єкт або масив, нове посилання при кожному рендері буде вважатися зміною. Тому в масиві залежностей потрібно передавати примітивні значення або стабільні посилання (наприклад, з useRef або useMemo).
За даними React Core Team — Optimization Guide (2024), вартість мемоізації включає три компоненти: алокацію масиву залежностей на кожен рендер, обхід і порівняння елементів через Object.is і потенційний overhead від збирання сміття при перестворенні. Для компонента з сотнями useCallback-обгорток це може стати помітним — тому селективність у використанні хука критична.
| Сценарій | Без useCallback | З useCallback |
|---|---|---|
| Створення функції | Нова на кожен рендер | Та ж при стабільних deps |
| Передача в React.memo | Дитина ререндериться | Дитина не ререндериться |
| В масиві useEffect | Ефект перезапускається | Ефект стабільний |
| Overhead | Мінімальний | Порівняння залежностей + пам'ять |
Широко поширений міф, що useCallback автоматично покращує продуктивність. Насправді, в ізоляції (без React.memo) useCallback навіть трохи сповільнює додаток через cost порівняння залежностей. Реальну користь хук приносить тільки в трьох сценаріях: запобігання ререндерам React.memo-компонентів, стабільність колбеків у useEffect і передача в кастомні хуки, які залежать від reference equality.
Правило просте: поки не виявиш проблему з продуктивністю через React DevTools Profiler — не використовуй useCallback. React-команда неодноразово підкреслювала, що попередня оптимізація — корінь зла. Спочатку напиши чистий код без мемоізації, виміряй, знайди bottleneck у профілювальнику і тільки потім додавай useCallback там, де дійсно потрібно.
// Вимірювана оптимізація: Child обгорнутий у React.memo
const Child = React.memo(({ onClick }) => {
console.log('Child перерендерено');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('натиснуто');
}, []);
return <Child onClick={handleClick} />;
}
За даними Dan Abramov — Before You memo() (2024), понад 90% випадків використання useCallback у open-source проектах — надлишкові. Розробники обгортають кожну функцію «про всяк випадок», не вимірюючи ефект. Альтернатива: якщо дочірній компонент важкий і його ререндер дорогий — React.memo + useCallback виправдані. Якщо дочірній компонент легкий — ререндер дешевший, ніж порівняння залежностей.
Перший сценарій — React.memo. Якщо дочірній компонент обгорнутий у React.memo і приймає колбек-функцію як проп, без useCallback дочірній компонент буде перемальовуватися при кожному рендері батька, навіть якщо його власні дані не змінилися. useCallback стабілізує посилання, і React.memo зможе коректно пропустити ререндер.
Другий сценарій — useEffect з колбеком у залежностях. Якщо функція передається в масив залежностей useEffect, кожне нове посилання буде перезапускати ефект. useCallback гарантує, що посилання стабільне, і ефект виконується тільки при зміні реальних даних, а не при кожному рендері. Це особливо важливо для підписок і запитів.
// useCallback для стабільної залежності useEffect
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // стабільне посилання, ніколи не перестворюється
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // ефект виконується тільки коли props.id змінюється
Головна відмінність між useCallback та useMemo в тому, що мемоізує кожен з них. useCallback мемоізує функцію: useCallback(fn, deps) повертає fn (ту ж саму або попередню версію). useMemo мемоізує результат виклику функції: useMemo(() => computeExpensive(a, b), [a, b]) повертає обчислене значення, а не функцію.
Технічно useCallback — це синтаксичний цукор над useMemo: useCallback(fn, deps) еквівалентний useMemo(() => fn, deps). Цей синтаксис існує тільки для читабельності — щоб розробник явно бачив, що мемоізується саме функція, а не значення. Різниці в продуктивності між useCallback та useMemo з функцією немає — вони генерують однаковий код.
| Хук | Мемоізує | Синтаксис | Використання |
|---|---|---|---|
| useCallback | Функцію (посилання) | useCallback(fn, deps) | Колбеки для дочірніх компонентів |
| useMemo | Результат обчислення | useMemo(() => value, deps) | Дорогі обчислення, мемоізація об'єктів |
// Вони еквівалентні:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
Найпоширеніша помилка — безглузде обгортання всіх функцій у useCallback без React.memo на дочірніх компонентах. Якщо дочірній компонент не обгорнутий у React.memo, він все одно буде ререндеритися при кожному рендері батька, незалежно від того, чи змінюється посилання на колбек. useCallback без React.memo — це витрати без вигоди.
// ❌ Марно: немає React.memo на дитині
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child все одно ререндериться без React.memo
// ❌ Stale closure: відсутня залежність
const handle = useCallback(() => {
console.log(count); // count завжди 0 — stale closure!
}, []);
// ✅ Правильно: включити залежності
const handle = useCallback(() => {
console.log(count);
}, [count]);
Проблема stale closure у useCallback вирішується зазначенням усіх використовуваних змінних у масиві залежностей. eslint-plugin-react-hooks з exhaustive-deps автоматично перевіряє, що всі змінні з тіла колбека присутні в масиві. Якщо колбек використовує setState, який не змінюється між рендерами, можна безпечно вказати його в deps — React гарантує стабільність setState.
Часто задавані питання
Ні. useCallback має сенс тільки в трьох випадках: дочірній компонент обгорнутий у React.memo, функція використовується в масиві залежностей useEffect або функція передається в кастомний хук, що залежить від reference equality. В інших випадках useCallback додає overhead без користі. React-команда рекомендує спочатку писати без оптимізацій і додавати їх за результатами профілювання.
Для простих компонентів — нова функція на кожен рендер трохи швидше, оскільки useCallback витрачає ресурси на порівняння залежностей та алокацію масиву. Для компонентів з глибокими деревами React.memo — useCallback виграє, запобігаючи ререндеру тисяч дочірніх елементів. Вимірюй і порівнюй, а не гадай — використовуй React DevTools Profiler для об'єктивної оцінки.
Так, useCallback працює з async-функціями точно так само, як і з синхронними. Хук мемоізує саму функцію, а результат (Promise) повертається щоразу при виклику. Асинхронна функція всередині useCallback — поширений патерн для стабільних колбеків завантаження даних, які використовуються в useEffect: const fetchData = useCallback(async (id) => {...}, []).
Використовуй React DevTools Profiler — він покаже, які компоненти ререндеряться і чому. Для програмної перевірки додай console.log або useWhyDidYouUpdate — бібліотеку, яка логує причину ререндера. Основні причини: змінився проп (включаючи колбек-посилання), змінився state або контекст. Якщо useCallback не допомагає — перевір, що всі залежності вказані коректно.
Навіть без React.memo useCallback може бути корисним у комбінації з useMemo для value контексту. Якщо ти передаєш об'єкт з функціями в Context.Provider, оберни створення об'єкта в useMemo, а кожну функцію — в useCallback. Це запобіжить ререндеру всіх споживачів контексту при зміні однієї з функцій. Але для прямої передачі колбеків у пропси без React.memo користі від useCallback немає.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.