Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Яка різниця між useCallback i useMemo?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useCallback** повертає мемоізоване посилання на функцію; **useMemo** повертає мемоізоване обчислене значення. `useCallback(fn, deps)` - це буквально `useMemo(() => fn, deps)`. ```jsx const doubled = useMemo(() => count * 2, [count]); // кешує значення const handleClick = useCallback(() => setCount(c => c + 1), []); // кешує посилання на функцію ``` **Ключове:** `useMemo` для дорогих обчислень, `useCallback` для стабільних функцій-пропів у мемоізованих дочірніх компонентах.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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](/questions/react-memo) дочірніх компонентів. ### Коли використовувати - Фільтрація або сортування великого масиву на кожному рендері? `useMemo` для результату. - Передаєш колбек як проп до дочірнього компонента з `React.memo`? `useCallback` для стабільності посилання. - Функція у [масиві залежностей useEffect](/questions/useeffect)? `useCallback`, щоб не отримати нескінченний цикл. - `count * 2` або `name.toUpperCase()`? Нічого з цього. Рахуй напряму. ### Порівняльна таблиця | Аспект | useMemo | useCallback | |---|---|---| | Повертає | Обчислене значення (будь-який тип) | Посилання на функцію | | Фабрика запускається | Тільки при зміні залежностей | Тільки при зміні залежностей | | Основне застосування | Дорогі обчислення | Стабільні колбеки як пропи | | З 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` підсвітить це одразу після збереження файлу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.