Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке React.memo і навіщо він потрібен». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React.memo** - це HOC, який обгортає функціональний компонент і пропускає повторний рендер, якщо пропси не змінились (поверхневе порівняння). ```jsx const Button = memo(({ label }) => <button>{label}</button>); // Кастомне порівняння memo(Component, (prev, next) => prev.id === next.id); ``` **Головне:** для функцій-пропсів обов'язково використовуй `useCallback`, інакше порівняння хибне при кожному рендері.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React.memo** - це компонент вищого порядку ([HOC](/questions/what-is-hoc)), який обгортає функціональний компонент і пропускає його повторний рендер, якщо пропси не змінились. ## Теорія ### TL;DR - React за замовчуванням ре-рендерить кожен дочірній компонент, коли батьківський рендериться заново, навіть якщо пропси дочірнього ідентичні. React.memo додає перевірку перед рендером. - Порівняння поверхневе (shallow): примітиви порівнюються за значенням, об'єкти і функції - за посиланням. - Використовуй, коли React Profiler показує, що компонент ре-рендериться з незмінними пропсами при кожному оновленні батька. - Для функцій і об'єктів в пропсах завжди додавай [`useCallback`](/questions/what-is-usecallback) або [`useMemo`](/questions/what-is-usememo), інакше порівняння буде хибним кожного разу. - Другий аргумент дозволяє задати кастомну логіку порівняння. ### Швидкий приклад Без memo `Button` рендериться при кожній зміні стану батька. Інлайн-функція `onClick` - це новий об'єкт при кожному рендері, тому навіть обгортка в memo без `useCallback` нічого не дасть: ```jsx import { memo, useState, useCallback } from 'react'; const Button = memo(({ onClick }) => { console.log('Button rendered'); return <button onClick={onClick}>Click</button>; }); function App() { const [count, setCount] = useState(0); // Те саме посилання між рендерами const handleClick = useCallback(() => console.log('clicked'), []); return ( <> <Button onClick={handleClick} /> <button onClick={() => setCount(c => c + 1)}>+1</button> </> ); } ``` `Button` рендериться один раз. Натискання `+1` оновлює `count` і ре-рендерить `App`, але `handleClick` зберігає своє посилання. Порівняння memo повертає рівність, і `Button` не рендериться знову. ### Ключова різниця Стандартна робота React: при рендері батька ре-рендеряться всі діти, навіть якщо їхній вивід не зміниться. `React.memo` вставляє поверхневу перевірку `Object.is` по кожному пропсу перед викликом функції компонента. Якщо всі пропси збіглись, React повертає закешований результат без виклику функції і без порівняння DOM. В деревах з великою кількістю листових вузлів, які рідко отримують нові дані, це помітно скорочує CPU-роботу. ### Коли використовувати - Елементи списків, які більшість часу не змінюються: обгортай кожен `<ListItem />` в memo, щоб незмінені елементи пропускали рендер при оновленні одного. - Компоненти з повільним рендером: графіки, великі таблиці з фільтрацією, вкладені списки. - Чисто відображувальні компоненти без локального стану. - React Profiler показує, що компонент займає значну частину часу кадру при незмінених пропсах. Не використовуй memo, коли: - Компонент малий і рендериться менш ніж за 0.1 мс. - Пропси змінюються при кожному рендері (інлайн-функція без `useCallback` завжди дає нове посилання). - Немає конкретних даних профайлера. Memo додає накладні витрати навіть там, де нічого не економить. ### Як React.memo працює всередині React зберігає попередні пропси і останній `ReactElement` у fiber-вузлі компонента. При наступному рендері запускається `shallowEqual`: перебір `Object.keys`, `Object.is` для кожної пари значень. Якщо всі ключі збіглись, повертається закешований елемент і функція твого компонента взагалі не викликається. Накладні витрати на порівняння - приблизно 1-2 мікросекунди. Виграш є тільки якщо зекономлений рендер дорожчий за цю вартість. В React 18 з concurrent mode memo також спрацьовує під час `startTransition`. Термінові оновлення (прямий ввід користувача) не відкладаються, і там memo запускається свіжо. ### Кастомне порівняння Коли поверхневого порівняння недостатньо, передай другий аргумент: ```jsx const UserCard = memo(({ user }) => { return <div>{user.name}</div>; }, (prevProps, nextProps) => { // true = пропустити рендер, false = дозволити рендер return prevProps.user.id === nextProps.user.id; }); ``` Повернути `true` означає, що пропси вважаються рівними. Але обережно з функціями глибокого порівняння: повільна перевірка може коштувати більше, ніж той рендер, який намагаєшся уникнути. ### Типові помилки **1. Інлайн-функції в пропсах** ```jsx // Помилка: нове посилання на функцію при кожному рендері <MemoChild onClick={() => alert(1)} /> // Виправлення const handleAlert = useCallback(() => alert(1), []); <MemoChild onClick={handleAlert} /> ``` `Object.is(() => {}, () => {})` завжди `false`. Порівняння хибне щоразу, і memo нічого не дає. **2. Об'єктні літерали, створені під час рендеру** ```jsx // Помилка: нове посилання при кожному рендері const user = { name: 'Alice', details: { age: 30 } }; return <UserProfile user={user} />; // Виправлення const user = useMemo(() => ({ name: 'Alice', details: { age: 30 } }), []); ``` Поверхневе порівняння перевіряє посилання на об'єкт, не його вміст. Новий літерал - нове посилання. **3. Мутація об'єктів пропсів на місці** Якщо передати `{ items: [] }` і мутувати масив без заміни посилання, memo бачить те саме посилання і пропускає рендер. UI показує застарілі дані. Завжди роби іммутабельні оновлення. **4. Мемоізація дрібних компонентів без профілювання** Компонент, що рендериться за 0.05 мс, коштує більше в накладних витратах порівняння, ніж економить. Flamegraph у React DevTools точно покаже, де витрачається час. Перевір там перед тим, як додавати memo. **5. Нестабільний проп `children`** ```jsx // Кожен рендер створює новий React-елемент <MemoChild> <span>label</span> </MemoChild> ``` `children` порівнюється за посиланням, як і будь-який інший проп. Новий JSX-вираз - новий об'єкт. Стабілізуй через `useMemo` або виноси статичних дітей у константу рівня модуля. На практиці найпоширеніша проблема - забути `useCallback` на обробниках подій, що тихо нейтралізує memo в кожному компоненті, який їх отримує. ### Де зустрічається - **React Window**: row-компоненти мемоізовані для списків з 10 000+ елементів. - **Material-UI**: `TableRow` обгорнуто в memo, щоб уникнути ре-рендерів при прокрутці гриду. - **TanStack Table**: рендерери комірок мемоізовані, щоб пропускати роботу при сортуванні і фільтрації. - **TanStack Query**: згенеровані хуки мемоізують вивід селекторів за тим же принципом, що `reselect` для Redux. - **Next.js**: memo позначає межі client-компонентів всередині server-дерев, щоб обмежити область ре-рендерів. ### Follow-up питання **Q:** Як працює поверхневе порівняння, якщо проп - це об'єкт? **A:** React перебирає `Object.keys` і викликає `Object.is` для кожного значення. Примітиви порівнюються за значенням. Об'єкти, масиви і функції - за посиланням. Два різних об'єктних літерали з однаковим вмістом не рівні з точки зору `Object.is`. **Q:** Коли React.memo погіршує продуктивність? **A:** Коли компонент рендериться швидше, ніж виконується порівняння. Рендер за 0.05 мс проти 0.1-0.3 мс на перевірку пропсів нічого не економить. Профілюй через React DevTools перед тим, як додавати memo. **Q:** Яка різниця між React.memo і useMemo? **A:** `React.memo` обгортає компонент і кешує його вивід на основі рівності пропсів. `useMemo` - хук, що кешує будь-яке обчислене значення всередині компонента. Часто їх використовують разом: memo на межі компонента, `useMemo` для стабілізації об'єкта, що передається вниз як проп. **Q:** Чи можна контролювати порівняння вручну? **A:** Так. Другий аргумент - це `(prevProps, nextProps) => boolean`. Повернути `true` = пропустити рендер, `false` = дозволити. Функції глибокого порівняння тут працюють, але додають свою вартість, тому вимірюй перед використанням. **Q:** Як React.memo поводиться з concurrent-режимом React 18? **A:** Memo працює з `startTransition`. Під час низькопріоритетних переходів React може перервати і перезапустити рендери, і порівняння memo виконується при кожній спробі. Термінові оновлення не відкладаються, тому там memo запускається свіжо. Fiber-вузол відстежує тип компонента через тег `MemoComponent`. ## Приклади ### Базовий: мемоізований статусний бейдж ```jsx import { memo, useState } from 'react'; const StatusBadge = memo(({ status }) => { console.log('StatusBadge rendered'); return <span className={`badge-${status}`}>{status}</span>; }); function Dashboard() { const [count, setCount] = useState(0); return ( <div> <StatusBadge status="active" /> <button onClick={() => setCount(c => c + 1)}>Кліків: {count}</button> </div> ); } ``` `StatusBadge` рендериться один раз. Кожен наступний клік оновлює `count` і ре-рендерить `Dashboard`, але `status` залишається `"active"`. Порівняння рядків проходить, бейдж пропускає рендер. ### Середній: мемоізований елемент списку задач ```jsx import { memo, useState, useCallback } from 'react'; const TodoItem = memo(({ id, text, completed, onToggle }) => { console.log(`TodoItem ${id} rendered`); return ( <label> <input type="checkbox" checked={completed} onChange={() => onToggle(id)} /> {text} </label> ); }); function TodoList() { const [todos, setTodos] = useState([ { id: 1, text: 'Купити продукти', completed: false }, { id: 2, text: 'Написати тести', completed: false }, ]); const handleToggle = useCallback((id) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }, []); return todos.map(todo => ( <TodoItem key={todo.id} {...todo} onToggle={handleToggle} /> )); } ``` Перемикання першої задачі створює новий об'єкт тільки для неї. Пропси другої (`id`, `text`, `completed`, `onToggle`) залишаються ідентичними, рендер пропускається. В списку з 200 елементів одне оновлення пропускає 199 рендерів. ### Просунутий: коли memo не спрацьовує через вкладені об'єкти ```jsx import { memo, useState, useMemo } from 'react'; const UserProfile = memo(({ user }) => { console.log('UserProfile rendered'); return <div>{user.name} - {user.details.age}</div>; }); function App() { const [count, setCount] = useState(0); // Помилка: новий об'єкт при кожному рендері const user = { name: 'Alice', details: { age: 30 } }; return ( <> <UserProfile user={user} /> <button onClick={() => setCount(c => c + 1)}>+1</button> </> ); } ``` Кожен клік створює новий літерал `user`. Поверхневе порівняння бачить нове посилання і ре-рендерить компонент. Memo нічого не дає. Виправлення: стабілізувати об'єкт. ```jsx const user = useMemo(() => ({ name: 'Alice', details: { age: 30 } }), []); ``` Тепер `user` зберігає своє посилання між рендерами, і memo працює як очікується.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.