Що спричиняє повторний рендер?
Ключова ідея
Virtual DOM - це основа, на якій React реалізує життєвий цикл компонента.
React не працює напряму з реальним DOM при кожній зміні. Замість цього:
- Будує віртуальне дерево (Virtual DOM) - JS-об'єкти, що описують структуру інтерфейсу.
- При зміні стану (
state,props) React створює нову версію віртуального дерева. - Порівнює її зі старою версією (процес reconciliation).
- Обчислює мінімальний набір змін, які потрібно застосувати до реального DOM.
- Застосовує їх у 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 |
Приклад: крок за кроком
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('effect');
return () => console.log('cleanup');
});
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}- Монтування
- React викликає
Counter()-> створює Virtual DOM для<button>0</button>. - Порівнювати поки що нема з чим -> створює DOM-елемент
<button>. - Після коміту викликає
useEffect().
render -> diff -> commit -> useEffect- Оновлення (
setCount)
- React знову викликає
Counter()-> нове Virtual DOM:<button>1</button>. - Порівнює старе (
<button>0</button>) з новим (<button>1</button>) -> бачить різницю в тексті. - Змінює тільки текст у DOM.
- Викликає cleanup старого ефекту -> потім новий
useEffect.
render -> diff -> minimal DOM update -> cleanup -> new useEffect- Розмонтування
- React видаляє вузол Virtual DOM
Counter. - Видаляє елемент
<button>з DOM. - Викликає
cleanup()зuseEffect.
unmount -> cleanup -> remove DOMЗв'язок з Render і Commit phases
| Фаза | Робота з Virtual DOM | Життєвий цикл |
|---|---|---|
| Render phase | React перестворює Virtual DOM (у пам'яті) | render() компонента, читання state, обчислення JSX |
| Commit phase | React оновлює реальний DOM, базуючись на diff | useLayoutEffect, useEffect, componentDidMount, componentDidUpdate, cleanup |
Ключова думка
Virtual DOM - це "робоча область", де React вирішує, що потрібно змінити. Життєвий цикл - це "часова шкала" того, коли ці зміни відбуваються і коли ти можеш втрутитися в цей процес.
Візуально
(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 всередині компонента
Найочевидніше:
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 перерендерить і дитину.
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(), перемалюються:
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 батька:
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 вважає його новим елементом -> розмонтовує старий, монтує новий.
{list.map(item => (
<Row key={item.id} data={item} />
))}Якщо id змінився -> Row створюється заново (а не оновлюється).
6. Зміна батьківського Context.Provider, навіть без зміни значення
React викликає всіх споживачів заново, якщо новий об'єкт value створюється при кожному рендері:
<ThemeContext.Provider value={{ color: 'black' }}>Тут при кожному виклику створюється новий об'єкт -> useContext бачить нове значення -> ререндер.
Щоб уникнути - мемоізуй value:
<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 | Немає змін | Ні |
Як уникнути зайвих ререндерів
React.memo(Component)- мемоізація за пропсами.useMemo()- мемоізація обчислень всередині компонента.useCallback()- мемоізація функцій, що передаються дітям.useContextSelector()або розбиття контекстів.- Не створювати нові об'єкти/функції прямо в JSX.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.