Методи життєвого циклу компонентів у React
Методи життєвого циклу компонентів (component lifecycle methods) в React — це функції класових компонентів, які React викликає автоматично в три конкретні моменти: коли компонент з'являється в DOM, оновлюється або видаляється.
Теорія
TL;DR
- Три фази: Монтування (компонент створено), Оновлення (змінились props або state), Демонтування (компонент видалено)
- Порядок при монтуванні:
constructor→render→componentDidMount. Завжди саме так. - Побічні ефекти (API-запити, таймери) йдуть у
componentDidMount, а не вrenderчиconstructor - Хуки відтворюють усі три фази через
useEffectз масивом залежностей - Новий код: хуки. Існуючі класові компоненти: lifecycle методи
Швидкий приклад
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. DidMountconstructor виконується першим, 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:
// Неправильно - виконується при кожному рендері, блокує 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:
componentDidMount() {
this.timer = setInterval(tick, 1000);
// Без очищення = витік пам'яті при кожному переході
}
componentWillUnmount() {
clearInterval(this.timer); // Саме для цього тут і є
}Відсутність очищення в componentWillUnmount — найпоширеніший витік пам'яті в React-продакшені. У React Router app компоненти монтуються і демонтуються при кожній навігації, тому інтервали і підписки накопичуються.
Виклик setState у componentWillUnmount:
componentWillUnmount() {
this.setState({ done: true }); // Warning: can't update unmounted component
}Компонент вже видалено. React ігнорує оновлення і виводить попередження. Замість цього використовуй булевий прапорець, який перевіряється в componentDidUpdate.
Відсутність перевірки prevProps у componentDidUpdate:
// Неправильно - нескінченний цикл
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
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
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 і очищенням
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 на демонтованому компоненті.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.