Олег Марков
Хуки жизненного цикла компонентов - полное руководство для разработчиков
Введение
Хуки жизненного цикла (lifecycle hooks) — это специальные методы или функции, которые вызываются фреймворком на разных этапах жизни компонента. С их помощью вы можете «встроиться» в процесс создания, обновления и уничтожения компонента и выполнить нужный код в строго определённый момент.
Чтобы не оставаться в теории, я буду опираться на примеры из популярных фреймворков: Angular, React и Vue. Смотрите, я покажу вам, как в разных системах идеи жизненного цикла реализуются по-разному, но логика этапов остаётся похожей.
Вам важно понимать две вещи:
- У каждого компонента есть жизненный цикл — от первого появления на экране до удаления.
- Хуки позволяют управлять этим циклом: подписываться на события, загружать данные, чистить ресурсы и оптимизировать работу.
Давайте разберём основные этапы жизненного цикла и посмотрим, как это реализуется на практике.
Основные этапы жизненного цикла компонента
Этапы жизненного цикла в общем виде
Независимо от фреймворка, жизненный цикл компонента чаще всего можно описать следующими этапами:
- Инициализация (создание)
- Монтаж (появление в DOM)
- Обновление (реакция на изменение данных или входных параметров)
- Размонтирование / уничтожение (удаление из DOM и освобождение ресурсов)
Давайте коротко определим каждый этап, а затем посмотрим на конкретные lifecycle-hooks в разных фреймворках.
Инициализация
На этом этапе:
- создаётся экземпляр компонента
- инициализируются его поля, состояние, пропсы или инпуты
- подготавливаются зависимости (например, через DI в Angular)
В этот момент компонент ещё не отрисован в DOM. Поэтому доступ к DOM-элементам и реальному размеру элементов обычно отсутствует или ограничен.
Монтаж (первый рендер в DOM)
Компонент впервые попадает в DOM:
- создаются реальные DOM-узлы
- появляются ссылки на элементы (например, через шаблонные ссылки или рефы)
- можно измерять размеры и позиции элементов
- можно запускать эффекты, завязанные на DOM (например, инициализировать сторонние библиотеки)
Здесь удобно:
- запрашивать данные с сервера (когда нужно сразу что-то показать)
- стартовать таймеры, подписки, слушатели событий
Обновление
На этом этапе:
- меняются входные параметры компонента (props / inputs)
- меняется внутреннее состояние (state / data / signals)
- фреймворк повторно рендерит компонент
Здесь жизненно важно:
- не создавать бесконечные циклы обновления
- аккуратно работать с побочными эффектами
- оптимизировать количество перерисовок
Размонтирование / уничтожение
Компонент удаляется из DOM:
- нужно отписаться от подписок
- остановить таймеры
- очистить слушатели событий
- освободить ресурсы (например, отписаться от WebSocket)
Если вы забываете это делать, возникают утечки памяти и «призрачные» обработчики, которые продолжают работать, хотя компонент уже не виден.
Lifecycle-hooks в Angular
В Angular lifecycle-hooks представлены в виде интерфейсов и методов, которые вы реализуете в классе компонента. Давайте посмотрим на основные.
Основные lifecycle-hooks Angular
Список основных хуков:
ngOnChangesngOnInitngDoCheckngAfterContentInitngAfterContentCheckedngAfterViewInitngAfterViewCheckedngOnDestroy
Не обязательно использовать все. Обычно достаточно нескольких ключевых: ngOnInit, ngOnChanges, ngOnDestroy, иногда ngAfterViewInit.
Давайте разберём каждый.
ngOnInit — инициализация компонента
Этот хук вызывается один раз после первой инициализации входных свойств @Input.
Пример:
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div>
<h3>{{ userName }}</h3>
<p *ngIf="loaded">Данные загружены</p>
</div>
`
})
export class UserCardComponent implements OnInit {
// Входное свойство, передается родителем
@Input() userId!: number;
loaded = false;
// Хук жизненного цикла - срабатывает один раз при инициализации
ngOnInit(): void {
// Здесь мы можем, например, запросить данные по userId
// Инициировать подписки и прочую логику, связанную с запуском компонента
this.loadUser();
}
private loadUser(): void {
// Здесь могла бы быть реальная HTTP-загрузка
// Для примера просто поставим флаг
this.loaded = true;
}
}
Здесь я размещаю пример, чтобы вам было проще понять: ngOnInit удобен для первоначальной инициализации, когда все входные данные уже готовы.
ngOnChanges — реакция на изменение @Input
Этот хук вызывается при каждом изменении входных свойств @Input, включая первое.
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>Текущее значение - {{ value }}</div>
`
})
export class CounterComponent implements OnChanges {
@Input() value = 0;
// Хук срабатывает при каждом изменении входных свойств
ngOnChanges(changes: SimpleChanges): void {
// Проверяем, изменилось ли конкретное свойство value
if (changes['value']) {
const prev = changes['value'].previousValue;
const curr = changes['value'].currentValue;
// Здесь можно отреагировать на изменение
// Например, сделать логирование или сбросить внутреннее состояние
console.log('value изменилось с', prev, 'на', curr);
}
}
}
Как видите, этот код выполняет простую задачу — реагирует на изменения инпутов. Это удобно, когда поведение компонента зависит от внешних данных.
ngAfterViewInit — доступ к DOM после рендера
Иногда вам нужно работать с элементами шаблона: измерять их размеры, инициализировать сторонние плагины и т. д. Для этого в Angular есть ngAfterViewInit.
import {
Component,
AfterViewInit,
ViewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-panel',
template: `
<div #panelRef class="panel">
Контент панели
</div>
`
})
export class PanelComponent implements AfterViewInit {
// Получаем ссылку на DOM-элемент через ViewChild
@ViewChild('panelRef') panelRef!: ElementRef<HTMLDivElement>;
// Хук вызывается после инициализации представления
ngAfterViewInit(): void {
// Теперь можно работать с реальным DOM-элементом
const el = this.panelRef.nativeElement;
// Например, прочитать его ширину
const width = el.offsetWidth;
console.log('Ширина панели', width);
// Или инициализировать стороннюю библиотеку
}
}
Здесь важно: в ngOnInit элемент ещё не готов, а в ngAfterViewInit уже можно безопасно работать с DOM.
ngOnDestroy — очистка ресурсов
ngOnDestroy вызывается перед тем, как Angular уничтожит компонент. Здесь вы освобождаете ресурсы.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-ticker',
template: `
<div>Текущий тик - {{ tick }}</div>
`
})
export class TickerComponent implements OnInit, OnDestroy {
tick = 0;
private sub?: Subscription;
ngOnInit(): void {
// Запускаем поток тиков каждую секунду
// Сохраняем подписку в поле класса
this.sub = interval(1000).subscribe(value => {
this.tick = value;
});
}
ngOnDestroy(): void {
// Важно отписаться, чтобы избежать утечек памяти
if (this.sub) {
this.sub.unsubscribe();
}
}
}
Обратите внимание: если вы не отписываетесь от подписок, компонент продолжит обновляться, даже будучи уже удалённым с экрана.
Lifecycle-hooks в React (class и function компоненты)
В React жизненный цикл исторически был реализован через методы классовых компонентов. Позже появились хуки (React hooks) для функциональных компонентов. Сейчас в новых проектах чаще используют функциональный подход, но понимание обоих вариантов полезно.
Этапы жизненного цикла в классовых компонентах
Основные методы:
constructorcomponentDidMountcomponentDidUpdatecomponentWillUnmountshouldComponentUpdate(для оптимизации)
Давайте разберём на примере.
import React from 'react';
class Timer extends React.Component {
// Конструктор - место для инициализации state
constructor(props) {
super(props);
// Внутреннее состояние компонента
this.state = {
seconds: 0
};
// Привязываем методы к this при необходимости
this.reset = this.reset.bind(this);
}
// Хук - компонент вставлен в DOM
componentDidMount() {
// Запускаем интервал и сохраняем ID
this.intervalId = setInterval(() => {
// Обновляем состояние каждую секунду
this.setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);
}
// Хук - компонент обновился
componentDidUpdate(prevProps, prevState) {
// Реагируем только при определенных изменениях
if (prevState.seconds !== this.state.seconds) {
console.log('Прошло секунд -', this.state.seconds);
}
}
// Хук - компонент будет удален
componentWillUnmount() {
// Очищаем интервал, чтобы избежать утечек
clearInterval(this.intervalId);
}
reset() {
// Сбрасываем счетчик
this.setState({ seconds: 0 });
}
render() {
// Рендерим JSX на основе текущего состояния
return (
<div>
<div>Секунд с начала - {this.state.seconds}</div>
<button onClick={this.reset}>Сбросить</button>
</div>
);
}
}
Здесь вы видите связку:
componentDidMount— старт подписок, таймеров, запросов.componentDidUpdate— реакция на обновления.componentWillUnmount— очистка.
Хук useEffect в функциональных компонентах
В функциональных компонентах все три этапа (mount, update, unmount) часто реализуются через один хук — useEffect.
Давайте разберемся на примере.
import React, { useEffect, useState } from 'react';
function Timer() {
// Внутреннее состояние - количество секунд
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Этот код запускается после монтирования компонента
// и после каждого обновления зависимостей (если они есть)
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Возвращаем функцию очистки - она вызовется при размонтировании
// и перед повторным выполнением эффекта
return () => {
clearInterval(id);
};
}, []); // Пустой массив - эффект выполнится только при монтировании и размонтировании
// Рендерим значение
return <div>Секунд с начала - {seconds}</div>;
}
Здесь важно:
- Код внутри
useEffectбез возвращения функции — это побочный эффект (подписки, запросы и т. д.). - Функция, которую вы возвращаете из
useEffect, — это аналогcomponentWillUnmountдля этого эффекта.
Если нужно реагировать на изменение конкретных данных, указываем их в массиве зависимостей.
useEffect(() => {
// Этот код запустится, когда изменится props.userId
// Здесь можно загрузить новые данные для пользователя
// Функция очистки (если нужно что-то отменить)
return () => {
// Очистка перед следующей загрузкой или размонтированием
};
}, [props.userId]); // Зависимость - id пользователя
Таким образом:
- монтирование — запуск эффекта
- обновление — повторный запуск эффекта при изменении зависимостей
- размонтирование — вызов функции очистки
Lifecycle-hooks во Vue 3
Во Vue 3 есть два стиля: Options API и Composition API. Хуки жизненного цикла есть в обоих вариантах, но вызываются по-разному.
Жизненный цикл во Vue Options API
В классическом стиле (Options API) вы определяете методы-хуки прямо в объекте компонента.
Основные хуки:
beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted
Чаще всего на практике используются mounted, beforeUnmount и иногда created.
Пример:
export default {
// Локальное состояние компонента
data() {
return {
count: 0,
timerId: null
};
},
// Хук - компонент смонтирован в DOM
mounted() {
// Запускаем таймер, обновляющий счетчик
this.timerId = setInterval(() => {
this.count++;
}, 1000);
},
// Хук - компонент будет размонтирован
beforeUnmount() {
// Очищаем таймер, чтобы избежать утечек
if (this.timerId) {
clearInterval(this.timerId);
}
},
// Шаблон компонента
template: `
<div>Счетчик Vue - {{ count }}</div>
`
};
Как видите, структура похожа на Angular и React: есть момент монтирования (mounted), есть момент перед уничтожением (beforeUnmount).
Жизненный цикл во Vue 3 Composition API
С Composition API всё завязано на функции-хуки из vue: onMounted, onUnmounted, onUpdated и др.
Давайте посмотрим, как это выглядит.
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
// Создаем реактивное состояние
const count = ref(0);
let timerId = null;
// Хук - компонент смонтирован
onMounted(() => {
// Запускаем таймер
timerId = setInterval(() => {
count.value++;
}, 1000);
});
// Хук - компонент будет размонтирован
onUnmounted(() => {
// Очищаем таймер
if (timerId) {
clearInterval(timerId);
}
});
// Возвращаем данные и методы для шаблона
return {
count
};
},
template: `
<div>Счетчик Composition API - {{ count }}</div>
`
};
Здесь вы вижите, как хуки жизненного цикла вызываются прямо внутри setup. Это удобно тем, что вы можете группировать логику по смыслу, а не по типу (данные, методы, computed и так далее).
Типичные сценарии использования lifecycle-hooks
Теперь давайте отойдем от особенностей фреймворков и посмотрим, какие задачи чаще всего решают хуки жизненного цикла.
Загрузка данных при монтировании
Классический сценарий: при первом рендере компонента нужно загрузить данные с сервера.
Angular:
ngOnInit(): void {
// Здесь мы вызываем сервис для загрузки данных
// и подписываемся на результат
this.userService.getUser(this.userId).subscribe(user => {
// Сохраняем загруженные данные в состояние компонента
this.user = user;
});
}
React:
useEffect(() => {
let cancelled = false;
async function loadUser() {
// Выполняем асинхронный запрос
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
// Проверяем, что компонент еще не размонтирован
if (!cancelled) {
setUser(data);
}
}
loadUser();
// Функция очистки - помечаем, что компонент размыкается
return () => {
cancelled = true;
};
}, [userId]); // Перезапуск при изменении userId
Vue 3:
onMounted(async () => {
// Выполняем загрузку данных
const res = await fetch(`/api/users/${userId.value}`);
const data = await res.json();
// Обновляем реактивное состояние
user.value = data;
});
Работа с DOM и сторонними библиотеками
Вам может понадобиться инициализировать библиотеку, которая работает напрямую с DOM (например, слайдер, графики, редактор текста).
Angular:
ngAfterViewInit(): void {
// Инициализация сторонней библиотеки с использованием DOM-элемента
this.slider = new SliderLibrary(this.sliderRef.nativeElement, {
autoplay: true
});
}
ngOnDestroy(): void {
// Важно корректно уничтожить инстанс библиотеки
this.slider.destroy();
}
React:
useEffect(() => {
// Получаем доступ к DOM через ref
const instance = new SliderLibrary(ref.current, { autoplay: true });
// Возвращаем функцию очистки
return () => {
instance.destroy();
};
}, []);
Vue 3:
onMounted(() => {
// Используем шаблонную ссылку на элемент
sliderInstance.value = new SliderLibrary(sliderRef.value, {
autoplay: true
});
});
onUnmounted(() => {
// Уничтожаем экземпляр библиотеки
sliderInstance.value.destroy();
});
Главная идея одинакова: инициализация в хуке «после монтирования» и уничтожение в хуке «перед размонтированием».
Управление подписками и событиями
Хуки жизненного цикла — удобное место для подписки на события и последующей отписки.
Angular:
ngOnInit(): void {
// Подписываемся на изменения маршрута
this.routeSub = this.route.params.subscribe(params => {
// Обрабатываем новые параметры
this.userId = params['id'];
});
}
ngOnDestroy(): void {
// Отписываемся от маршрута
this.routeSub.unsubscribe();
}
React:
useEffect(() => {
// Добавляем обработчик события resize окна
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
// Очищаем обработчик при размонтировании
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Подписка один раз
Vue 3:
onMounted(() => {
// Обработчик изменения размера окна
const onResize = () => {
width.value = window.innerWidth;
};
window.addEventListener('resize', onResize);
onUnmounted(() => {
// Удаляем обработчик
window.removeEventListener('resize', onResize);
});
});
Как видите, здесь хуки жизненного цикла помогают «связать» начало и конец жизненного пути подписки.
Распространённые ошибки при работе с lifecycle-hooks
Теперь давайте посмотрим на типичные проблемы, с которыми сталкиваются разработчики.
Дублирование логики и утечки памяти
Частая ошибка — создавать подписки или интервалы в хуке, который вызывается многократно, без соответствующей очистки.
React (опасный пример):
useEffect(() => {
// Каждый раз при изменении count создается новый интервал
const id = setInterval(() => {
console.log(count);
}, 1000);
// Здесь нет функции очистки - интервал никогда не очищается
}, [count]); // Интервал добавляется при каждом изменении
Этот пример приведёт к накоплению интервалов и утечкам. Правильный вариант:
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
// Возвращаем функцию очистки - предыдущий интервал будет очищен
return () => clearInterval(id);
}, [count]);
Выполнение тяжёлой логики в «горячих» хуках
Хук, который вызывается часто (например, при каждом обновлении), не должен содержать тяжёлые операции.
Angular:
ngDoCheck(): void {
// Этот код будет выполнен при каждом цикле обнаружения изменений
// Нельзя помещать сюда дорогие операции
}
Если туда поместить сложные вычисления или синхронные запросы, вы получите проблемы с производительностью.
Обращение к DOM раньше времени
Ещё одна распространённая ошибка — попытка работать с DOM в хуке, где DOM ещё не готов.
Angular:
ngOnInit(): void {
// В большинстве случаев здесь еще нельзя безопасно работать с ViewChild
console.log(this.panelRef.nativeElement); // Может быть undefined
}
Корректно делать это в ngAfterViewInit, как мы разбирали выше.
Советы по выбору подходящего lifecycle-hook
Чтобы вам было проще ориентироваться, давайте сформулируем простые правила выбора хуков.
Когда использовать хуки инициализации
Используйте:
- Angular —
ngOnInit - React —
useEffectс пустым массивом зависимостей - Vue 3 —
onMounted/created(Options API)
для задач:
- первичная загрузка данных
- настройка начального состояния
- старт подписок, которые не зависят от DOM (например, WebSocket)
Когда использовать хуки, завязанные на DOM
Используйте:
- Angular —
ngAfterViewInit - React —
useEffect(обычно тоже с пустыми зависимостями) - Vue —
mounted/onMounted
когда:
- нужен доступ к реальным DOM-элементам
- нужно измерить размеры блока
- требуется инициализировать плагин, который работает с DOM
Когда использовать хуки уничтожения
Используйте:
- Angular —
ngOnDestroy - React — функцию очистки, возвращаемую из
useEffect - Vue —
beforeUnmount/onUnmounted
для:
- отписки от подписок, событий
- очистки таймеров и интервалов
- уничтожения экземпляров сторонних библиотек
Когда реагировать на изменения входных данных
Используйте:
- Angular —
ngOnChanges - React —
useEffectс массивом зависимостей - Vue —
watch(для наблюдения за реактивными значениями)
когда:
- нужно выполнить логику при изменении props / inputs
- требуется перерасчитать какие-то данные
- нужно повторно загрузить данные при смене параметров
Хуки жизненного цикла позволяют вам точно контролировать поведение компонента на каждом этапе его жизни: от создания до уничтожения. С их помощью удобно:
- отделять инициализацию от обновлений
- корректно освобождать ресурсы
- выстраивать предсказуемую структуру кода
Если вы чётко понимаете, в какой момент что происходит с компонентом, вы реже будете сталкиваться с «магическими» багами и утечками памяти. При работе с конкретным фреймворком полезно держать под рукой схему его жизненного цикла и соотносить с ней логику в хуках.
Частозадаваемые технические вопросы по lifecycle-hooks
1. Как протестировать код внутри lifecycle-hook?
В модульных тестах не нужно напрямую вызывать внутренние механизмы фреймворка. Обычно вы:
- создаёте компонент через тестовый раннер (TestBed в Angular, render в React Testing Library, mount в Vue Test Utils)
- затем проверяете видимый эффект работы хука
Например, в React:
- рендерите компонент
- ждёте выполнения
useEffectчерезwaitFor - проверяете, что UI или состояние изменилось
То есть вы не тестируете сам хук, вы тестируете поведение компонента, которое он обеспечивает.
2. Можно ли вызывать lifecycle-hook вручную?
В большинстве случаев — нет. Хуки вызываются фреймворком, и ручной вызов ломает его модель работы. Если вам нужно переиспользовать логику из хука, лучше:
- вынести её в отдельную функцию или сервис
- вызывать эту функцию и из хука, и из других мест
Так вы сохраните предсказуемость и тестируемость кода.
3. Что делать, если нужно общий код для нескольких хуков?
Иногда часть логики одинакова для ngOnInit и ngOnChanges, или для разных useEffect. В таких случаях:
- выносите общую часть в отдельную функцию
- вызывайте её из разных хуков с нужными параметрами
Так вы избежите дублирования и сможете проще изменять поведение.
4. Как отменить асинхронный запрос при размонтировании компонента?
Подход зависит от стека, но общий принцип:
- храните токен отмены или флаг активности
- в хуке уничтожения помечайте, что компонент размонтирован
- внутри асинхронного кода проверяйте этот флаг перед обновлением состояния
В fetch можно использовать AbortController, в Axios — CancelToken, в RxJS — отписку от Observable.
5. Как избежать конфликтов нескольких эффектов, зависящих от одних и тех же данных?
Если вы используете, например, несколько useEffect с одинаковыми зависимостями, они могут запускаться в неопределённом порядке и влиять друг на друга. Чтобы избежать этого:
- группируйте связанную логику в одном эффекте, если есть зависимость между шагами
- выносите повторяющиеся шаги в общий слой (хук, сервис, модуль)
- чётко разделяйте эффекты по ответственности (один отвечает за запросы данных, другой — за синхронизацию с локальным хранилищем и так далее)
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев