Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що спричиняє повторний рендер?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Повторний рендер (re-render)** - це повторний виклик функції компонента, щоб React зрозумів, який JSX він має відобразити; це відбувається при зміні `state` всередині компонента, зміні `props` від батька, зміні значення контексту, ререндері батька або зміні `key` елемента. **Ключове:** React порівнює нове значення зі старим через `Object.is()`, і якщо `state` не змінився фактично, ререндер не викликається.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## Ключова ідея > **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 ``` 2. **Оновлення (**`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 ``` 3. **Розмонтування** - React видаляє вузол Virtual DOM `Counter`. - Видаляє елемент `<button>` з DOM. - Викликає `cleanup()` з `useEffect`. ```javascript 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 вирішує, **що потрібно змінити**. > Життєвий цикл - це "часова шкала" того, **коли** ці зміни відбуваються і **коли ти можеш втрутитися** в цей процес. --- ## Візуально ```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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.