Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Методи життєвого циклу компонентів у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Методи життєвого циклу компонентів** (component lifecycle methods) — це функції, які React викликає автоматично при монтуванні, оновленні та демонтуванні класового компонента. Порядок монтування: `constructor` → `render` → `componentDidMount`. ```jsx // Еквівалент через хуки охоплює всі три фази: useEffect(() => { // логіка componentDidMount + componentDidUpdate return () => { /* очищення componentWillUnmount */ }; }, [deps]); ``` **Ключове:** API-запити у `componentDidMount`, очищення у `componentWillUnmount`. У новому коді `useEffect` замінює всі три фази.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Методи життєвого циклу компонентів** (component lifecycle methods) в React — це функції класових компонентів, які React викликає автоматично в три конкретні моменти: коли компонент з'являється в DOM, оновлюється або видаляється. ## Теорія ### TL;DR - Три фази: Монтування (компонент створено), Оновлення (змінились props або state), Демонтування (компонент видалено) - Порядок при монтуванні: `constructor` → `render` → `componentDidMount`. Завжди саме так. - Побічні ефекти (API-запити, таймери) йдуть у `componentDidMount`, а не в `render` чи `constructor` - Хуки відтворюють усі три фази через `useEffect` з масивом залежностей - Новий код: хуки. Існуючі класові компоненти: lifecycle методи ### Швидкий приклад ```jsx class Timer extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; console.log('1. Constructor'); } componentDidMount() { // DOM готовий, тут безпечно запускати побічні ефекти this.timer = setInterval(() => { this.setState({ count: this.state.count + 1 }); }, 1000); console.log('3. DidMount - таймер запущено'); } componentWillUnmount() { clearInterval(this.timer); // запобігає витоку пам'яті console.log('Демонтування - таймер зупинено'); } render() { console.log('2. Render'); return <div>Лічильник: {this.state.count}</div>; } } // Виведе при монтуванні: 1. Constructor → 2. Render → 3. DidMount ``` `constructor` виконується першим, `render` будує virtual DOM, а `componentDidMount` спрацьовує коли реальний DOM готовий. Порядок незмінний. ### Три фази **Монтування** — коли компонент вперше з'являється в DOM: | Метод | Коли викликається | |-------|-------------------| | `constructor` | Екземпляр компонента створено, state ініціалізовано | | `static getDerivedStateFromProps` | Перед кожним render (рідко потрібен) | | `render` | Повертає JSX. Має бути чистою функцією. | | `componentDidMount` | Після того як DOM відмальовано і готовий | **Оновлення** відбувається коли змінюються props або state: | Метод | Коли викликається | |-------|-------------------| | `static getDerivedStateFromProps` | Перед перерендером | | `shouldComponentUpdate` | Може повернути `false` і пропустити перерендер | | `render` | Генерує оновлений JSX | | `getSnapshotBeforeUpdate` | Зчитує DOM перед оновленням (позиція скролу тощо) | | `componentDidUpdate` | Після оновлення DOM, тут безпечно для побічних ефектів | **Демонтування** має один метод: `componentWillUnmount`. Спрацьовує перед тим як React видаляє компонент. Тут прибирають таймери, слухачі подій і незавершені запити. ### Головна різниця від хуків Lifecycle методи мають строгий порядок виклику, який забезпечує Fiber reconciler React. `render` ніколи не виконується під час демонтування. `componentDidMount` завжди спрацьовує після фіксації DOM. Ця передбачуваність спрощує розуміння складних сценаріїв. `useEffect` об'єднує всі три фази в одну функцію. Невірний масив залежностей призводить або до застарілих даних, або до нескінченного перерендеру. Lifecycle методи унеможливлюють цей баг за своєю природою. ### Коли що використовувати - Початкове завантаження даних: `componentDidMount` - Повторний запит при зміні props: `componentDidUpdate` з порівнянням `prevProps` - Пропуск зайвих перерендерів: `shouldComponentUpdate` (або `React.memo`) - Очищення таймерів і підписок: `componentWillUnmount` - Синхронізація state з props: `getDerivedStateFromProps` (рідко, краще `useEffect`) - Фіксація скролу перед оновленням DOM: `getSnapshotBeforeUpdate` ### Lifecycle методи vs хуки | Аспект | Lifecycle методи | Хуки (`useEffect`) | |--------|-----------------|---------------------| | Фази | Монтування / Оновлення / Демонтування (9+ методів) | Один ефект з масивом залежностей | | Порядок | Строгий і гарантований | Після render; залежності керують повторним запуском | | Очищення | `componentWillUnmount` | Функція повернення з `useEffect` | | Продуктивність | `shouldComponentUpdate` для точного контролю | `React.memo` + `useMemo` в React 18+ | | Складність | Більше API для запам'ятовування | Простіше, але залежності бувають каверзними | | Коли використовувати | Легасі кодова база | Весь новий код | ### Як React виконує це всередині Fiber reconciler будує робоче дерево під час узгодження (reconciliation). Він може призупиняти роботу щоб дати пріоритет введенню користувача над фоновими оновленнями. Після фіксації дерева в реальному DOM React викликає `componentDidMount` або `componentDidUpdate`. Демонтування запускає `componentWillUnmount` перед видаленням дерева. У конкурентному режимі React 18 ефекти можуть групуватись і відкладатись, що частково пояснює відмінності в поведінці хуків і класових методів. ### Типові помилки **API-запит у `render`:** ```jsx // Неправильно - виконується при кожному рендері, блокує UI render() { const data = fetch('/api/todos'); return <div>{data}</div>; } // Правильно componentDidMount() { fetch('/api/todos') .then(res => res.json()) .then(data => this.setState({ data })); } ``` `render` має бути чистою функцією. Ніяких асинхронних викликів, ніяких побічних ефектів. **Відсутність очищення в `componentWillUnmount`:** ```jsx componentDidMount() { this.timer = setInterval(tick, 1000); // Без очищення = витік пам'яті при кожному переході } componentWillUnmount() { clearInterval(this.timer); // Саме для цього тут і є } ``` Відсутність очищення в `componentWillUnmount` — найпоширеніший витік пам'яті в React-продакшені. У React Router app компоненти монтуються і демонтуються при кожній навігації, тому інтервали і підписки накопичуються. **Виклик `setState` у `componentWillUnmount`:** ```jsx componentWillUnmount() { this.setState({ done: true }); // Warning: can't update unmounted component } ``` Компонент вже видалено. React ігнорує оновлення і виводить попередження. Замість цього використовуй булевий прапорець, який перевіряється в `componentDidUpdate`. **Відсутність перевірки `prevProps` у `componentDidUpdate`:** ```jsx // Неправильно - нескінченний цикл componentDidUpdate() { this.fetchData(); // спрацьовує після кожного оновлення, включно з тими що він сам спричинює } // Правильно componentDidUpdate(prevProps) { if (this.props.userId !== prevProps.userId) { this.fetchData(); } } ``` Завжди додавай умову в `componentDidUpdate`. Без неї — нескінченний цикл перерендерів. ### Де зустрічається в реальних проектах - React Router: `componentDidMount` завантажує дані конкретного маршруту (профіль, деталі запису) - Redux-Observable: підписка на epic в `componentDidMount`, відписка в `componentWillUnmount` - Чат-застосунки: `getSnapshotBeforeUpdate` фіксує позицію скролу, `componentDidUpdate` відновлює її після завантаження нових повідомлень - Next.js: `componentDidMount` виконується тільки на клієнті, підходить для браузерних API типу `localStorage` - Міграція на React 18: заміна класових lifecycle методів на `useEffect` відкриває конкурентний рендеринг ### Питання на співбесіді **Q:** Який порядок монтування для батьківського і дочірнього компонентів? **A:** Constructor батька, render батька, constructor дочірнього, render дочірнього, `componentDidMount` дочірнього, потім `componentDidMount` батька. Дочірні компоненти завжди монтуються раніше ніж батько завершує монтування. **Q:** Чому не варто робити API-запити в `constructor`? **A:** `constructor` виконується до появи компонента в DOM. Немає гарантій щодо стану DOM, а в SSR (Next.js, Remix) це призводить до помилок гідратації. Правильне місце для запитів — `componentDidMount`. **Q:** Навіщо потрібен `getSnapshotBeforeUpdate`? **A:** Він виконується прямо перед тим як React застосовує зміни до DOM. Можна зчитати поточні значення DOM (позиція скролу) і передати їх у `componentDidUpdate` третім аргументом. Найпоширеніший кейс: збереження позиції скролу в чаті при завантаженні нових повідомлень вище поточного вьюпорта. **Q:** Чи може `shouldComponentUpdate` спричинити баги? **A:** Так. Якщо повернути `false` помилково, компонент перестає оновлюватись без жодної помилки. Та сама небезпека є в `React.memo` з кастомним компаратором. Завжди профілюй з React DevTools Profiler перед такою оптимізацією. **Q:** У конкурентному режимі React 18 lifecycle методи можуть спрацьовувати кілька разів? **A:** `render` може викликатись декілька разів перед фіксацією. Але `componentDidMount` і `componentDidUpdate` все одно спрацьовують один раз за фіксацію. Це одна з причин чому React 18 рекомендує хуки: семантика `useEffect` чіткіша в конкурентних сценаріях. ## Приклади ### Базовий: відстеження порядку lifecycle ```jsx class LifecycleDemo extends React.Component { constructor(props) { super(props); this.state = { name: props.name }; console.log('1. constructor'); } static getDerivedStateFromProps(props, state) { console.log('2. getDerivedStateFromProps'); return null; } componentDidMount() { console.log('4. componentDidMount - DOM готовий'); } componentDidUpdate(prevProps, prevState) { console.log('5. componentDidUpdate'); } componentWillUnmount() { console.log('6. componentWillUnmount - очищення тут'); } render() { console.log('3. render'); return <div>Привіт, {this.state.name}</div>; } } ``` Запусти це у браузері щоб побачити точний порядок. `getDerivedStateFromProps` спрацьовує перед кожним `render`, включно з оновленнями. Це здивовує більшість розробників вперше. ### Середній: список завдань з API ```jsx class TodoList extends React.Component { state = { todos: [], loading: true }; componentDidMount() { this.fetchTodos(); } componentDidUpdate(prevProps) { // Повторний запит тільки коли змінився filter if (this.props.filter !== prevProps.filter) { this.fetchTodos(); } } fetchTodos() { this.setState({ loading: true }); fetch(`/api/todos?filter=${this.props.filter}`) .then(res => res.json()) .then(todos => this.setState({ todos, loading: false })); } render() { if (this.state.loading) return <div>Завантаження...</div>; return ( <ul> {this.state.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } } ``` `componentDidMount` обробляє початкове завантаження. `componentDidUpdate` обробляє повторні запити при зміні props без дублювання логіки. Перевірка `prevProps` запобігає нескінченному циклу. ### Складний: пошук з debounce і очищенням ```jsx class SearchResults extends React.Component { state = { results: [] }; componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { this.scheduleFetch(); } } scheduleFetch = () => { // Стрілкова функція зберігає правильний контекст this clearTimeout(this.timeout); this.timeout = setTimeout(() => { fetch(`/api/search?q=${this.props.query}`) .then(res => res.json()) .then(data => this.setState({ results: data.results })); }, 300); }; componentWillUnmount() { clearTimeout(this.timeout); // скасовуємо запит якщо компонент зникає } render() { return ( <ul> {this.state.results.map(r => <li key={r.id}>{r.title}</li>)} </ul> ); } } ``` Два моменти роблять цей код коректним. Debounce виключає запит при кожному натисканні клавіші. Очищення в `componentWillUnmount` скасовує відкладений таймаут якщо компонент демонтується до закінчення 300мс, запобігаючи `setState` на демонтованому компоненті.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.