Skip to main content

Яка різниця між useCallback i useMemo?

useCallback зберігає стабільне посилання на функцію; useMemo зберігає обчислене значення. Обидва хуки приймають фабричну функцію і масив залежностей. Різниця - в тому, що вони повертають.

Теорія

TL;DR

  • useMemo кешує обчислене значення: відфільтрував масив один раз, результат живе до зміни залежностей
  • useCallback кешує посилання на функцію: той самий об'єкт функції між рендерами
  • Ключовий момент: useCallback(fn, deps) - це буквально useMemo(() => fn, deps)
  • Стабільна функція для пропу дочірнього компонента? useCallback. Дороге обчислення? useMemo
  • Для простих операцій не варто використовувати жоден з них - оверхед хука переважить користь

Короткий приклад

jsx
import { useState, useMemo, useCallback } from 'react'; function Counter() { const [count, setCount] = useState(0); const [other, setOther] = useState(0); // useMemo: перераховує тільки коли змінюється count const doubled = useMemo(() => count * 2, [count]); // useCallback: те саме посилання на функцію, якщо залежності не змінились const handleClick = useCallback(() => setCount(c => c + 1), []); return <button onClick={handleClick}>Count: {doubled} | other: {other}</button>; }

Зміниш other - doubled не перераховується. Зміниш count - doubled перераховується, але handleClick залишається тим самим посиланням.

Головна різниця

useMemo запускає фабрику і зберігає те, що вона повертає: число, рядок, об'єкт, масив. useCallback зберігає саму фабричну функцію без виклику. Тому якщо твоя фабрика повертає функцію через useMemo, ти отримаєш нове посилання щоразу (нестабільне). useCallback дає те саме посилання, яке потрібне для оптимізації React.memo дочірніх компонентів.

Коли використовувати

  • Фільтрація або сортування великого масиву на кожному рендері? useMemo для результату.
  • Передаєш колбек як проп до дочірнього компонента з React.memo? useCallback для стабільності посилання.
  • Функція у масиві залежностей useEffect? useCallback, щоб не отримати нескінченний цикл.
  • count * 2 або name.toUpperCase()? Нічого з цього. Рахуй напряму.

Порівняльна таблиця

АспектuseMemouseCallback
ПовертаєОбчислене значення (будь-який тип)Посилання на функцію
Фабрика запускаєтьсяТільки при зміні залежностейТільки при зміні залежностей
Основне застосуванняДорогі обчисленняСтабільні колбеки як пропи
З React.memoМемоізація об'єктів/масивівМемоізація функцій
ЕквівалентuseMemo(() => val, deps)useMemo(() => fn, deps)
Коли пропуститиПрості операціїФункція не іде в дочірній компонент

Як це працює всередині

React зберігає мемоізований результат у memoizedState fiber-вузла. При кожному рендері він порівнює залежності через Object.is (поверхневе порівняння). Якщо всі залежності збіглися - повертає закешоване значення, не викликаючи фабрику. Саме тому об'єкти і масиви у залежностях викликають постійний перерахунок: {} === {} завжди false.

Типові помилки

Пропущені залежності (застаріле замикання, stale closure):

jsx
// Помилка: b використовується, але не вказана в deps const sum = useMemo(() => a + b, [a]); // Завжди a + 2, ігнорує зміни b // Правильно const sum = useMemo(() => a + b, [a, b]);

eslint-plugin-react-hooks з правилом exhaustive-deps знаходить це автоматично. Додавай це правило в кожен React-проект.

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

jsx
// Безглуздо: handleClick нікуди як проп не передається const handleClick = useCallback(() => setCount(c => c + 1), []); return <button onClick={handleClick}>Click</button>;

Я бачив кодові бази, де useCallback додали до кожного хендлера після того як команда прочитала про оптимізацію. Profiler не показав різниці, зате з'явились зайвий оверхед і складніші баги із залежностями. Якщо функція не іде в мемоізований дочірній компонент і не в масив залежностей - пиши її inline.

Об'єкт або масив у залежностях без мемоізації:

jsx
// Перераховує кожен рендер: options - нове посилання щоразу const result = useMemo(() => compute(options), [options]); // Правильно: мемоізуй і залежність const stableOptions = useMemo(() => ({ limit: 10 }), []); const result = useMemo(() => compute(stableOptions), [stableOptions]);

Де це зустрічається в реальних проектах

  • Пошук/фільтр: useMemo для фільтрації 1000+ елементів без перерахунку при несповіщених змінах стану
  • TanStack Query: загортає результати запитів у useMemo для стабільних посилань
  • Кастомні хуки: useCallback для функцій, що повертаються хуком, щоб споживачі не ре-рендерились зайво
  • Redux-селектори: стабільні action creators через патерни з useCallback

Follow-up питання

Q: Що станеться, якщо передати об'єктний літерал напряму у масив залежностей?
A: Поверхневе порівняння провалиться щоразу. {} === {} - це false, тому хук перераховуватиме кожен рендер. Загортай об'єкт у власний useMemo або деструктуруй до примітивів.

Q: Якщо useCallback(fn, deps) дорівнює useMemo(() => fn, deps), навіщо два хуки?
A: Для читабельності. useCallback сигналізує: це стабільна функція-колбек. useMemo сигналізує: це кешоване значення. Той самий механізм, різна семантика.

Q: Чи може мемоізація погіршити продуктивність?
A: Так. Кожен хук виділяє пам'ять і порівнює залежності. Для простих операцій типу count * 2 цей оверхед більший за вартість самого обчислення. Перевіряй через React DevTools Profiler перш ніж додавати memo.

Q: Чому у React 18 Strict Mode фабрика викликається двічі?
A: React навмисно подвійно викликає фабрики у dev-режимі, щоб виявити побічні ефекти в коді, який має бути чистим. У production запускається один раз.

Q: (Senior) Що змінює React Compiler у React 19?
A: Компілятор може автоматично додавати мемоізацію там де вона потрібна, що зменшує потребу писати useMemo і useCallback вручну. Механізм під капотом залишається тим самим.

Приклади

Фільтрація списку через useMemo та useCallback

jsx
function TodoList({ todos, filter }) { // Без useMemo: фільтрує всі todos при кожному ре-рендері батька const visibleTodos = useMemo(() => todos.filter(todo => todo.text.toLowerCase().includes(filter.toLowerCase()) ), [todos, filter] ); // Стабільне посилання: мемоізовані TodoItem не ре-рендеряться зайво const handleDelete = useCallback((id) => { console.log('Deleting todo', id); }, []); return visibleTodos.map(todo => ( <TodoItem key={todo.id} todo={todo} onDelete={handleDelete} /> )); }

visibleTodos перераховується тільки при зміні todos або filter. handleDelete тримає те саме посилання, тому мемоізовані TodoItem пропускають ре-рендер при несповіщених змінах батька.

Баг із застарілим замиканням (stale closure) у deps

jsx
function BadSum() { const [a, setA] = useState(1); const [b, setB] = useState(2); // Баг: b відсутня у залежностях const sum = useMemo(() => a + b, [a]); // Після оновлення b до 3 - sum показує a + 2 return ( <> <button onClick={() => setB(b => b + 1)}>B: {b}</button> <div>Sum: {sum}</div> </> ); }

Натискання B збільшує b у стані, але мемо залишається застарілим, бо b не в масиві залежностей. Рішення: [a, b]. Правило exhaustive-deps підсвітить це одразу після збереження файлу.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?