Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Причини повторного рендерингу компонентів у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Перерендеринг компонента React** відбувається при зміні стану через `useState` або `useReducer`, при перерендерингу батька, або при оновленні значення контексту. ```jsx setCount(count + 1); // зміна стану → перерендеринг ``` **Ключове:** перерендеринг батька каскадно передається всім дітям; `React.memo` блокує це через shallow-порівняння пропсів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Перерендеринг** - це коли React викликає функцію компонента знову, будує нове віртуальне DOM-дерево і порівнює його з попереднім. ## Теорія ### TL;DR - Зміна стану завжди ставить ре-рендер у чергу, навіть якщо значення виглядає однаково - Перерендеринг батька каскадно передається всім дочірнім компонентам, якщо їх не захистити `React.memo` - Зміна контексту перерендерить кожного споживача в дереві, а не тільки найближчого - Аналогія: перерендеринг схожий на те, як кухня в ресторані передруковує талон замовлення. Нове замовлення (стан), крик менеджера (перерендеринг батька), або особливий запит (контекст) - все це запускає новий друк, щоб перевірити що змінилось - Правило: запусти React DevTools Profiler перш ніж додавати будь-яку мемоізацію ### Швидкий приклад ```jsx import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); console.log('Counter рендериться'); // логується при кожному рендері return ( <button onClick={() => setCount(count + 1)}> Кліків: {count} </button> ); } // Кожен клік → setCount → компонент запускається знову → лог ``` Клік на кнопку викликає `setCount`, React перезапускає `Counter`, будує нове дерево, порівнює і оновлює тільки текст кнопки. Ось і весь цикл. ### Що запускає перерендеринг **Зміна стану.** Виклик `setCount` або будь-якого сеттера ставить ре-рендер у чергу. React 18 батчить кілька змін стану в одному обробнику події в один рендер, тому два `setState` в одному кліку дадуть один рендер, не два. **Зміна пропсів.** За замовчуванням будь-який перерендеринг батька перезапускає всі дочірні компоненти, навіть якщо пропси не змінились. React не порівнює пропси перед викликом дочірнього компонента. Саме тому існує `React.memo`. **Перерендеринг батька.** Це найпоширеніша причина несподіваних ре-рендерів у продакшені. Оновлення лічильника в батьківському компоненті змушує всіх дітей перезапускатись, навіть тих, що не отримують жодних пропсів. **Зміна контексту.** Кожен компонент, що викликає `useContext(MyContext)`, перерендериться при зміні значення контексту. Якщо передавати новий літерал об'єкта в провайдер при кожному рендері (`value={{ theme: 'dark' }}`), це нове посилання кожного разу, тому всі споживачі перерендеряться, навіть якщо дані однакові. **Диспатч через useReducer.** Виклик `dispatch` завжди тригерить перерендеринг компонента з редюсером і далі каскадно вниз по дереву. ### Головна різниця: перерендеринг і DOM-коміт Перрендеринг і оновлення DOM - це два окремих кроки. React викликає функцію компонента (перерендеринг), будує нове fiber-дерево, порівнює з попереднім, і тільки потім записує мінімальний набір змін у реальний DOM (коміт). Компонент може перерендеритись десятки разів без жодних DOM-мутацій, якщо результат однаковий. Саме тому React DevTools Profiler показує кількість рендерів окремо від часу малювання. ### Коли оптимізувати - **Повільні рядки в довгих списках** - обгортай рядки в `React.memo`, стабілізуй дані через `useMemo` - **Колбеки в пропсах, що ламають memo** - обгортай їх у `useCallback`, щоб посилання залишалось стабільним між рендерами батька - **Контекст, що надто широко тригерить ре-рендери** - розбий один великий контекст на менші або обгортай значення в `useMemo` - **Обчислення, що перезапускаються при кожному рендері** - кешуй через `useMemo` - **Перед усім цим** - запусти React DevTools Profiler і переконайся, що проблема реальна. Оптимізація без вимірювань - це здогади ### Як React обробляє перерендеринги всередині Планувальник React ставить fiber-вузли в чергу при зміні стану або пропсів. Рекончайлер проходить по fiber-дереву, порівнює попередні та нові пропси через shallow-рівність і позначає вузли для оновлення. У React 18 автоматичний батчинг (automatic batching) групує кілька змін стану в асинхронних контекстах (проміси, `setTimeout`) в один прохід. Фаза коміту потім записує тільки diff-зміни в реальний DOM. Тому 50 ре-рендерів можуть дати всього 3 DOM-оновлення. ### Типові помилки **Мутація стану напряму** ```jsx const [arr, setArr] = useState([1, 2]); arr.push(3); setArr(arr); // те саме посилання → React пропускає перерендеринг ``` React порівнює посилання через `Object.is`. Те саме посилання означає відсутність оновлення. Виправлення: `setArr([...arr, 3])` - новий масив з новим посиланням. **Інлайн-об'єкти або функції в пропсах ламають memo** ```jsx const Child = React.memo(({ user }) => <div>{user.name}</div>); function Parent() { const [count, setCount] = useState(0); const user = { name: 'Alice' }; // новий об'єкт при кожному рендері return <Child user={user} />; } // Child рендериться при кожному ре-рендері Parent, незважаючи на React.memo ``` `React.memo` робить shallow-порівняння. `{ name: 'Alice' } !== { name: 'Alice' }` - різні посилання. Виправлення: `const user = useMemo(() => ({ name: 'Alice' }), []);` **Літерал об'єкта як значення контексту** ```jsx <MyContext.Provider value={{ theme: 'dark' }}> ``` При кожному рендері батька створюється новий об'єкт. Всі споживачі перерендеряться. Виправлення: обгорни в `useMemo` або перенеси значення в стан. **Думати, що useEffect запобігає ре-рендерам** `useEffect` запускається після рендеру, не замість нього. Додавання ефекту не пропускає цикл рендерингу. Щоб уникнути зайвих рендерів, мемоізуй компонент або стабілізуй його пропси. **Інлайн-стрілочні функції в пропсах** ```jsx <Child onClick={() => doSomething()} /> ``` При кожному рендері батька створюється нове посилання на функцію. Якщо `Child` обгорнутий у `React.memo`, він все одно перерендериться, бо проп завжди виглядає новим. Виправлення: `const handleClick = useCallback(() => doSomething(), []);` ### Де зустрічається в реальному коді - **React TodoMVC** - масиви в стані тригерять рендер всього списку; `React.memo` на рядках для 1000+ елементів - **Redux Toolkit** - Immer створює нові посилання на стан при кожній дії, тому `useSelector` потребує точних селекторів - **Next.js** - зміна роутера передає нові пропси в компоненти сторінок; `useSWR` стабілізує посилання на дані - **React Query** - мутації кешу повідомляють тільки підписані observer-и запитів, а не все дерево - **Material-UI** - зміна теми каскадно торкається всіх styled-компонентів; стабільні селектори в `makeStyles` обмежують це ### Питання на співбесіді **Q:** Мій мемоізований компонент все одно рендериться. Чому? **A:** Перевір нестабільні посилання в пропсах: інлайн-функції, інлайн-об'єкти, значення контексту без `useMemo`. React DevTools Profiler покаже "why did this render" для кожного компонента. **Q:** Стан не змінився, але компонент перерендерився. Чому? **A:** Найімовірніше - перерендеринг батька. Обгорни компонент у `React.memo` і переконайся, що всі його пропси мають стабільні посилання. **Q:** Яка різниця між перерендерингом і DOM-комітом? **A:** Перерендеринг - це виклик функції компонента React. DOM-коміт - це запис змін у браузер. Можна мати багато рендерів і мало DOM-мутацій, якщо результат однаковий. **Q:** Як автоматичний батчинг React 18 впливає на рендери? **A:** До React 18 кожен `setState` у `setTimeout` або `Promise.then` давав окремий рендер. React 18 автоматично батчить всі в один прохід. Для неважливих оновлень є `startTransition`, щоб UI залишався чуйним під час важких рендерів. **Q:** Як оптимізувати список з 10 000 елементів, що рендериться повільно? **A:** Комбінуй windowing (react-window або react-virtual), `React.memo` на компонентах рядків і `useMemo` на трансформації даних. Windowing дає найбільший ефект, бо взагалі прибирає більшість DOM-вузлів з дерева. ## Приклади ### Базовий: стан запускає перерендеринг ```jsx import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); console.log('рендеринг'); // запускається при кожному кліку return ( <button onClick={() => setCount(count + 1)}> Кліків: {count} </button> ); } ``` Кожен клік передає нове значення `setCount`. React перезапускає `Counter`, логує і оновлює текст кнопки. Якщо викликати `setCount(count)` з тим самим значенням, React виконає один рендер і зупиниться. ### Середній: каскадний рендер без memo і з ним ```jsx function TodoList() { const [todos, setTodos] = useState([]); // порожній масив залежностей працює, бо використовується функціональна форма setTodos const addTodo = useCallback((text) => { setTodos(prev => [...prev, text]); }, []); return ( <div> <AddTodo onAdd={addTodo} /> <TodoItems todos={todos} /> </div> ); } // Без memo: рендериться при кожному оновленні батька // З memo: рендериться тільки коли змінюється масив todos const TodoItems = React.memo(({ todos }) => { console.log('TodoItems рендериться'); return todos.map((todo, i) => <div key={i}>{todo}</div>); }); ``` Без `React.memo` `TodoItems` рендериться кожного разу, коли батько оновлюється, навіть якщо `todos` не змінився. `memo` разом зі стабільним `addTodo` скорочує рендери до тих випадків, коли список справді змінюється. ### Просунутий: пастка з об'єктом у пропсах ```jsx const UserCard = React.memo(({ user }) => { console.log('UserCard рендериться'); // все одно логується return <div>{user.name}</div>; }); function Dashboard() { const [tick, setTick] = useState(0); // новий об'єкт при кожному рендері, shallow-порівняння завжди провалюється const user = { name: 'Alice', id: 1 }; return ( <> <button onClick={() => setTick(t => t + 1)}>Tick</button> <UserCard user={user} /> </> ); } // Виправлення: // const user = useMemo(() => ({ name: 'Alice', id: 1 }), []); ``` `React.memo` порівнює пропси за shallow-рівністю. Два об'єктних літерали `{ name: 'Alice' }` ніколи не рівні за посиланням, тому memo ніколи не блокує рендер. `useMemo` з порожнім масивом залежностей зберігає те саме посилання між рендерами. Я бачив цей баг у дашборді з 40+ картками, де кожен тік таймера перерендерував весь список через один інлайн-об'єкт стилів у пропсах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.