Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює useMemo і навіщо він потрібен». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)`useMemo` кешує обчислене значення між рендерами React і перераховує його лише при зміні залежностей. ```jsx const filtered = useMemo( () => items.filter(item => item.active), [items] // перерахунок тільки при зміні items ); ``` **Ключове:** використовуй для важких обчислень (фільтрація, сортування великих масивів) або коли потрібна стабільна референція об'єктів для дочірніх компонентів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useMemo** - це хук React, який кешує обчислене значення між рендерами і перераховує його лише тоді, коли змінюються вказані залежності. ## Теорія ### TL;DR - Уяви кухаря, який готує дорогий соус один раз за зміну і використовує його далі, поки ключові інгредієнти не змінились. - Без `useMemo` кожен рендер запускає обчислення знову, навіть якщо вхідні дані ті самі. - React зберігає результат і порівнює залежності через `Object.is`. Якщо нічого не змінилось, повертає закешоване значення. - Використовуй, коли обчислення займає більше ~5ms і залежності стабільні більшість рендерів. - Функції - `useCallback`, значення - `useMemo`. ### Швидкий приклад ```jsx import { useMemo, useState } from 'react'; const TodoList = ({ todos }) => { const [filter, setFilter] = useState('all'); // Без useMemo: фільтрація на КОЖНОМУ рендері, навіть якщо filter не змінився // const visible = todos.filter(t => t.status === filter); const visible = useMemo( () => todos.filter(t => t.status === filter), [todos, filter] // перераховуємо тільки при зміні цих значень ); return <ul>{visible.map(t => <li key={t.id}>{t.text}</li>)}</ul>; }; ``` Якщо батьківський компонент рендериться повторно, але `filter` і `todos` не змінились, `useMemo` повертає закешований масив. Фільтрація не відбувається. ### Головна різниця Без `useMemo` важкі функції запускаються на кожному рендері незалежно від того, чи змінились вхідні дані. Але є ще одна проблема: кожен рендер створює нове посилання на об'єкт або масив, що ламає рівність за посиланням для дочірніх компонентів і запускає їхні рендери теж. `useMemo` зберігає попередній результат у вузлі fiber компонента, порівнює кожну залежність через `Object.is` і пропускає callback, якщо нічого не змінилось. Один виклик рятує і від зайвих обчислень, і від каскадних рендерів нижче по дереву. ### Коли використовувати - Важка фільтрація, сортування або `reduce` по великих масивах (1 000+ елементів) - `useMemo`. - Похідний стан, який залишається стабільним більшість рендерів - `useMemo`. - Об'єкт або масив, що передається як проп до компонента з `React.memo` - `useMemo`. - Обчислення менше 1ms - просто напиши вираз inline. - Залежності змінюються на кожному рендері - `useMemo` тут не допоможе, кеш ніколи не спрацює. ### Як це працює всередині React зберігає кожен результат `useMemo` у зв'язаному списку `memoizedState` на вузлі fiber компонента. При першому рендері запускається `mountMemo` - зберігаються і значення, і масив залежностей. При повторному рендері запускається `updateMemo`: React проходить по залежностях через `Object.is` (це функція `areHookInputsEqual` у вихідному коді React) і якщо всі збігаються, повертає закешоване значення без виклику callback. Якщо хоча б одна залежність змінилась, callback запускається і результат зберігається. Два моменти, які варто знати. По-перше, `useMemo` виконується синхронно під час фази рендеру, тому async-callback всередині не підтримується. По-друге, кеш не постійний. React може його видалити при нестачі пам'яті або коли fiber відкидається при розмонтуванні. Це підказка для оптимізації, а не гарантоване сховище. ### useMemo vs useCallback | | `useMemo` | `useCallback` | |---|---|---| | Повертає | Значення | Функцію | | Сценарій | Обчислені дані, об'єкти, масиви | Обробники подій, callbacks | | Приклад | `useMemo(() => a + b, [a, b])` | `useCallback(() => doX(), [x])` | | Коли пропустити | Дешеве обчислення | Callback не передається дочірнім компонентам | ### Типові помилки **Нестабільний об'єкт у залежностях** ```jsx // Перераховує кожен рендер - { key: 'value' } це нове посилання щоразу const value = useMemo(() => heavyCalc(items), [items, { key: 'value' }]); // Виправлення: передай примітив напряму const value = useMemo(() => heavyCalc(items), [items, config.key]); ``` **Мемоізація дешевих операцій** ```jsx // Накладні витрати хука більші за економію const doubled = useMemo(() => a * 2, [a]); // Просто напиши inline const doubled = a * 2; ``` **Мутація мемоізованого значення** ```jsx const list = useMemo(() => items.filter(isActive), [items]); list.push(newItem); // Псує кеш - React очікує, що callback чистий // Виправлення: створи новий масив const withNew = [...list, newItem]; ``` **Відсутні залежності** ```jsx // userId змінюється, але useMemo ніколи не перераховує через порожній deps const result = useMemo(() => fetchData(userId), []); // Виправлення: додай userId до залежностей const result = useMemo(() => fetchData(userId), [userId]); ``` **Каскад вкладених useMemo** ```jsx const data1 = useMemo(() => heavy1(input), [input]); const data2 = useMemo(() => heavy2(data1), [data1]); // data1 змінився - data2 теж перераховується // Виправлення: об'єднай в один useMemo для всього пайплайну const { data1, data2 } = useMemo(() => { const d1 = heavy1(input); return { data1: d1, data2: heavy2(d1) }; }, [input]); ``` В продакшені я бачив, як такий каскадний патерн знищував продуктивність дашбордів із 4-5 похідними станами ланцюжком. Об'єднання в один `useMemo` скорочувало час рендеру на 60%. ### Де зустрічається в реальних проектах - TanStack Query мемоізує результати запитів, щоб стабільні дані не запускали зайві рендери нижче по дереву. - Redux Toolkit's `createSelector` із бібліотеки Reselect реалізує ту саму ідею, що й `useMemo`, але на рівні стору. - Next.js використовує мемоізовані похідні метадані при обробці `getStaticProps`. - `React.memo` + `useMemo` - стандартний підхід: `React.memo` пропускає рендер дочірнього компонента, а `useMemo` тримає пропси стабільними за посиланням, щоб пропуск реально спрацьовував. ### Питання на співбесіді **Q:** Яка часова складність порівняння залежностей у `useMemo`? **A:** O(n), де n - кількість залежностей. React проходить по кожній через `Object.is`. Тому масив залежностей краще тримати коротким, до 5 елементів. **Q:** Чи блокує `useMemo` рендеринг? **A:** Так. Callback виконується синхронно під час фази рендеру. Якщо обчислення справді важке і блокує UI, `useMemo` не вирішить проблему - треба виносити роботу з головного потоку. **Q:** У чому різниця між `useMemo` і `React.memo`? **A:** `useMemo` кешує значення всередині одного компонента. `React.memo` обгортає компонент і пропускає його рендер, якщо пропси не змінились. Вони вирішують різні проблеми, але добре доповнюють одне одного. **Q:** Що трапляється, якщо залежність - це об'єкт `Date`? **A:** `Object.is` порівнює за посиланням. `new Date()` на кожному рендері - це новий об'єкт, тому порівняння завжди провалюється і `useMemo` завжди перераховує. Використовуй примітив - числову мітку часу або ISO-рядок. **Q:** (Senior) Як concurrent mode впливає на кеш `useMemo`? **A:** У React 18 низькопріоритетна робота може бути перервана, а fiber відкинутий. Коли fiber відкидається, його `memoizedState` втрачається і при перезапуску `useMemo` обчислює з нуля. Для некритичних обчислень обгорни оновлення стану в `startTransition`, щоб React знав, що може відкласти цю роботу. ## Приклади ### Базовий: фільтрація списку товарів ```jsx import { useMemo, useState } from 'react'; const ProductList = ({ products }) => { const [search, setSearch] = useState(''); const filtered = useMemo( () => products.filter(p => p.name.toLowerCase().includes(search.toLowerCase())), [products, search] ); return ( <> <input value={search} onChange={e => setSearch(e.target.value)} /> <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul> </> ); }; ``` `filtered` перераховується тільки при зміні `products` або `search`. Введення тексту в input запускає перерахунок. Рендер батьківського компонента з тими самими пропсами - ні. ### Середній: обчислення статистики в todo-додатку ```jsx const TodoStats = ({ todos }) => { const { activeCount, completedCount } = useMemo(() => { let active = 0; let completed = 0; todos.forEach(todo => { if (todo.completed) completed++; else active++; }); return { activeCount: active, completedCount: completed }; }, [todos]); return <p>{activeCount} активних, {completedCount} завершених</p>; }; ``` Об'єкт, повернутий із `useMemo`, залишається тим самим посиланням між рендерами, поки `todos` не змінюється. Це важливо, якщо `TodoStats` обгорнутий у `React.memo` - без цього новий об'єкт щоразу ламав би пропуск рендеру. ### Просунутий: нестабільний об'єкт у залежностях ```jsx // Це не працює - config це новий об'єкт на кожному рендері батька const BadChart = ({ data, config }) => { const processed = useMemo( () => data.map(item => ({ ...item, color: config.theme })), [data, config] // config !== prev config, навіть якщо theme не змінився ); return <Chart data={processed} />; }; // Виправлення - деструктуруй примітив, який реально потрібен const GoodChart = ({ data, config }) => { const { theme } = config; const processed = useMemo( () => data.map(item => ({ ...item, color: theme })), [data, theme] // порівняння примітивів працює коректно ); return <Chart data={processed} />; }; ``` Цей edge case спотикає навіть досвідчених розробників. Лінтер не завжди його ловить, бо `config` технічно використовується всередині callback. Виправлення - деструктурувати на початку і передавати до масиву залежностей тільки примітиви.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.