Skip to main content

Що спричиняє повторний рендер?

Ключова ідея

Virtual DOM - це основа, на якій React реалізує життєвий цикл компонента.

React не працює напряму з реальним DOM при кожній зміні. Замість цього:

  1. Будує віртуальне дерево (Virtual DOM) - JS-об'єкти, що описують структуру інтерфейсу.
  2. При зміні стану (state, props) React створює нову версію віртуального дерева.
  3. Порівнює її зі старою версією (процес reconciliation).
  4. Обчислює мінімальний набір змін, які потрібно застосувати до реального DOM.
  5. Застосовує їх у commit phase.

І всі події життєвого циклу відбуваються навколо цих дій.


Як Virtual DOM пов'язаний з фазами життєвого циклу

Ось зв'язок поетапно:

ЕтапЩо робить React з Virtual DOMЯкі методи / хуки викликаються
Монтування (mount)React створює перше віртуальне дерево на основі JSX-компонентівuseEffect (після коміту), useLayoutEffect, componentDidMount
Оновлення (update)React створює нове віртуальне дерево, порівнює його зі старим -> знаходить відмінності (diffing)useEffect (cleanup + новий), useLayoutEffect, componentDidUpdate
Розмонтування (unmount)React видаляє вузол з віртуального дерева -> видаляє відповідні DOM-елементиcleanup-функції в useEffect, componentWillUnmount

Приклад: крок за кроком

javascript
function Counter() { const [count, setCount] = useState(0); useEffect(() => { console.log('effect'); return () => console.log('cleanup'); }); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
  1. Монтування
  • React викликає Counter() -> створює Virtual DOM для <button>0</button>.
  • Порівнювати поки що нема з чим -> створює DOM-елемент <button>.
  • Після коміту викликає useEffect().
javascript
render -> diff -> commit -> useEffect
  1. Оновлення (setCount)
  • React знову викликає Counter() -> нове Virtual DOM: <button>1</button>.
  • Порівнює старе (<button>0</button>) з новим (<button>1</button>) -> бачить різницю в тексті.
  • Змінює тільки текст у DOM.
  • Викликає cleanup старого ефекту -> потім новий useEffect.
javascript
render -> diff -> minimal DOM update -> cleanup -> new useEffect
  1. Розмонтування
  • React видаляє вузол Virtual DOM Counter.
  • Видаляє елемент <button> з DOM.
  • Викликає cleanup() з useEffect.
javascript
unmount -> cleanup -> remove DOM

Зв'язок з Render і Commit phases

ФазаРобота з Virtual DOMЖиттєвий цикл
Render phaseReact перестворює Virtual DOM (у пам'яті)render() компонента, читання state, обчислення JSX
Commit phaseReact оновлює реальний DOM, базуючись на diffuseLayoutEffect, useEffect, componentDidMount, componentDidUpdate, cleanup

Ключова думка

Virtual DOM - це "робоча область", де React вирішує, що потрібно змінити. Життєвий цикл - це "часова шкала" того, коли ці зміни відбуваються і коли ти можеш втрутитися в цей процес.


Візуально

javascript
(state змінився) ┌──────────────┐ Render phase │ ← будуємо нове Virtual DOM └──────────────┘ порівнюємо з попереднім (diffing) ┌──────────────┐ Commit phase │ ← оновлюємо реальний DOM └──────────────┘ викликаємо useEffect()

Хочеш, я намалюю схему життєвого циклу компонента (монтування -> оновлення -> розмонтування) із зазначенням, де React працює з Virtual DOM і де викликаються хуки (useEffect, useLayoutEffect, cleanup)?

Ви сказали:

Що спричиняє повторний рендер?

ChatGPT сказав:

Чудове запитання - розуміння що саме спричиняє повторний рендер (re-render) - це ключ до оптимізації React-застосунків. Давай розберемо все детально, без "магії": коли React вирішує викликати компонент заново і чому.


Що таке "рендер" узагалі?

"Рендер" - це виклик функції компонента, щоб React зрозумів, який JSX (тобто Virtual DOM) він має відобразити. Це не завжди означає зміну реального DOM!

React може викликати компонент заново, порівняти результат з попереднім Virtual DOM і... нічого не змінити в браузері, якщо все те саме.


Повторний рендер (re-render) відбувається, коли React вважає, що потрібно перерахувати JSX.

Ось усі причини, через які це відбувається


1. Зміна state всередині компонента

Найочевидніше:

javascript
function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; }
  • Кожен виклик setCount() -> React викликає Counter() заново.
  • Компонент перемальовується.
  • Потім React порівнює старий і новий Virtual DOM (diffing) -> і змінює тільки змінений текст у реальному DOM.

Навіть якщо setState встановить те саме значення (setCount(0) при count === 0), React порівняє старе і нове значення і не викличе ререндер, якщо вони рівні (порівняння Object.is()).


2. Зміна props, отриманих від батька

Якщо батько перерендерився і передав нові пропси (за посиланням або за значенням) - React перерендерить і дитину.

javascript
function Child({ value }) { console.log('render child'); return <div>{value}</div>; } function Parent() { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(c => c + 1)}>+</button> <Child value={count} /> {/* -> ререндер при зміні count */} </> ); }
  • Parent змінився -> React викликає Parent().
  • Новий Virtual DOM включає <Child value={newCount}>.
  • У Child змінився проп -> він теж ререндериться.

Навіть якщо батько змінив невикористовуваний стан, все одно відбудеться виклик функції-компонента, але React може оптимізувати це за допомогою React.memo.


3. Зміна контексту (useContext)

Якщо ти використовуєш контекст, і його значення змінилося - всі компоненти, які його читають через useContext(), перемалюються:

javascript
const ThemeContext = createContext('light'); function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { const theme = useContext(ThemeContext); // при зміні - ререндер return <div>{theme}</div>; }

Щоб не ререндерилися всі споживачі контексту, використовують селектори або розділяють контексти.


4. Зміна батька

Якщо батько ререндериться, то React за замовчуванням ререндерить усіх його дітей, тому що заново викликає JSX батька:

javascript
function Parent() { const [count, setCount] = useState(0); return ( <> <Child /> {/* викличеться заново */} <button onClick={() => setCount(c => c + 1)}>+</button> </> ); }

Навіть якщо Child не залежить від count, React все одно перестворить його JSX. Щоб цього уникнути, використовують React.memo(Child).


5. Зміна key у компонента

Якщо у компонента змінився key, React вважає його новим елементом -> розмонтовує старий, монтує новий.

javascript
{list.map(item => ( <Row key={item.id} data={item} /> ))}

Якщо id змінився -> Row створюється заново (а не оновлюється).


6. Зміна батьківського Context.Provider, навіть без зміни значення

React викликає всіх споживачів заново, якщо новий об'єкт value створюється при кожному рендері:

javascript
<ThemeContext.Provider value={{ color: 'black' }}>

Тут при кожному виклику створюється новий об'єкт -> useContext бачить нове значення -> ререндер. Щоб уникнути - мемоізуй value:

javascript
<ThemeContext.Provider value={useMemo(() => ({ color: 'black' }), [])}>

7. Оновлення через forceUpdate (рідко, але можливо)

У класових компонентах this.forceUpdate() явно змушує React викликати render() заново, навіть якщо state і props не змінилися.


Ключова таблиця

ПричинаЩо змінилосяСпричиняє повторний рендер?
setState() всередині компонентаТакТак
Новий props від батькаТакТак
Батько перерендеривсяНавіть без зміни propsТак
Значення контексту (useContext)ТакТак
Новий об'єкт/функція в пропсах (за посиланням)Так, якщо посилання змінилосяТак
Новий key у елементаТакТак (ремонтування)
setState() з тим самим значеннямНі (React порівняє Object.is())Ні
React.memo() + однакові propsНемає змінНі

Як уникнути зайвих ререндерів

  1. React.memo(Component) - мемоізація за пропсами.
  2. useMemo() - мемоізація обчислень всередині компонента.
  3. useCallback() - мемоізація функцій, що передаються дітям.
  4. useContextSelector() або розбиття контекстів.
  5. Не створювати нові об'єкти/функції прямо в JSX.

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

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

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