Стан та життєвий цикл
На цій сторінці представлено поняття стану та життєвого циклу у React-компоненті. Ви можете знайти детальний API-довідник по компонентах тут.
Розглянемо приклад відліку годинника з одного з попередніх розділів. У Рендерингу елементів ми дізналися лише один спосіб оновлення UI. Ми викликаємо ReactDOM.render(), щоб змінити відрендерний вивід інформації:
function tick() {
const element = (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render( element, document.getElementById('root') );}
setInterval(tick, 1000);У цьому розділі ми дізнаємося, як зробити компонент ‘Clock’ дійсно багаторазовим та інкапсульованим. Компонент сам налаштує свій таймер та оновлюватиметься кожну секунду.
Ми можемо почати з того, як виглядає годинник:
function Clock(props) {
return (
<div> <h1>Привіт, світе!</h1> <h2>Зараз {props.date.toLocaleTimeString()}.</h2> </div> );
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />, document.getElementById('root')
);
}
setInterval(tick, 1000);Однак, ми порушуємо важливу вимогу: той факт, що Clock встановлює таймер і оновлює UI кожну секунду, має бути деталлю реалізації Clock.
В ідеалі ми хочемо написати це один раз аби Clock оновлював себе сам:
ReactDOM.render(
<Clock />, document.getElementById('root')
);Аби це реалізувати, нам потрібно додати “стан”(“state”) до компонента Clock.
Стан подібний до пропсів, але він приватний і повністю контролюється компонентом.
Перетворення функції на клас
Ви можете перетворити функцію компонента Clock на клас у п’ять кроків:
- Створіть клас ES6 class з тим же ім’ям, що наслідує
React.Component. - Додайте до нього один порожній метод, який називається
render(). - Перемістіть тіло функції в метод
render(). - Замініть
propsнаthis.propsв тіліrender(). - Видаліть порожні оголошення функції які залишилися.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Clock тепер визначається як клас, а не як функція.
Метод render буде викликатися кожного разу, коли відбуватиметься оновлення. Але до тих пір, поки ми рендеремо <Clock /> в тому ж DOM-вузлі, буде використано лише один екземпляр класу Clock. Це дозволяє нам використовувати додаткові функції, такі як методи внутрішнього стану та життєвого циклу.
Додавання внутрішнього стану до класу
Ми перемістимо date з пропсів до стану в три етапи:
- Замінити
this.props.dateнаthis.state.dateу методіrender():
class Clock extends React.Component {
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}- Додайте конструктор класу, який присвоює
this.stateпочаткове значення:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Зверніть увагу на те, як ми передаємо props(пропси) базовому конструктору:
constructor(props) {
super(props); this.state = {date: new Date()};
}Компоненти класу повинні завжди викликати базовий конструктор з props.
- Видалити елемент
dateз елемента<Clock />:
ReactDOM.render(
<Clock />, document.getElementById('root')
);Пізніше ми додамо код таймера назад до самого компонента.
Результат виглядає так:
class Clock extends React.Component {
constructor(props) { super(props); this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
ReactDOM.render(
<Clock />, document.getElementById('root')
);Далі ми зробимо так, аби Clock сам налаштував свій таймер і оновлював себе кожну секунду.
Додавання методів життєвого циклу до класу
У додатках з багатьма компонентами, дуже важливо при знищенні компонентів звільняти ресурси, що використовуються.
Ми хочемо налаштувати таймер кожного разу, коли Clock буде передано DOM вперше. У React це називається “монтування”.
Ми також хочемо очистити цей таймер, коли DOM, створений компонентом Clock, видаляється. У React це називається “демонтування”.
Ми можемо оголосити спеціальні методи в класі компонента, які будуть викликані тоді, коли компонент монтується і демонтується:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() { }
componentWillUnmount() { }
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}Ці методи називаються “методами життєвого циклу”.
Метод componentDidMount() виконується після того, як вивід компонента був відрендерений у DOM. Це гарне місце для налаштування таймера:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }Зверніть увагу на те, як ми зберігаємо ідентифікатор таймера прямо у this (this.timerID).
Хоча this.props налаштовує сам React, а this.state має особливе значення, ви можете додавати додаткові поля до класу вручну, якщо потрібно зберегти те, що не бере участь у потоці даних (як ідентифікатор таймера).
У методі життєвого циклу componentWillUnmount(), ми очистимо таймер:
componentWillUnmount() {
clearInterval(this.timerID); }Нарешті, ми реалізуємо метод під назвою tick(), який компонент Clock буде запускати кожну секунду.
Він буде використовувати this.setState() для планування оновлення внутрішнього стану компонента:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() { this.setState({ date: new Date() }); }
render() {
return (
<div>
<h1>Привіт, світе!</h1>
<h2>Зараз {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);Тепер годинник тікає кожну секунду.
Давайте швидко повторимо, що відбувається, і порядок, в якому ці методи викликаються:
- Коли
<Clock />передається доReactDOM.render (), React викликає конструктор компонентаClock. ОскількиClockмає відображати поточний час, він ініціалізуєthis.stateз об’єктом, що включає поточний час. Пізніше ми оновимо цей стан. - React потім викликає метод
render()компонентаClock. Ось як React дізнається, що саме має відображатися на екрані. Потім React оновлює DOM, щоб він відповідав виводу рендераClock. - Коли виведення рендера
Clockвставляється в DOM, React викликає метод життєвого циклуcomponentDidMount(). Всередині нього компонентClockпросить браузер налаштувати таймер для виклику методу компонентаtick()один раз на секунду. - Кожну секунду браузер викликає метод
tick(). У цьому методі компонентClockпланує оновлення UI, викликаючиsetState()з об’єктом, що містить поточний час. Завдяки викликуsetState()React знає, що стан змінився, і знову викликає методrender(), щоб дізнатися, що має бути на екрані. Цього разуthis.state.dateв методіrender()буде відрізнятися і тому вивід рендера буде включати оновлений час. React оновлює DOM відповідно. - Якщо компонент
Clockколи-небудь буде видалений з DOM, React викличе метод життєвого циклуcomponentWillUnmount(), аби таймер зупинився.
Правильно використовувати стан
Є три речі, які ви повинні знати про setState().
Не змінюйте стан безпосередньо
Наприклад, це не буде повторно рендерити компонент:
// Wrong
this.state.comment = 'Привіт';Натомість використовуйте setState():
// Correct
this.setState({comment: 'Привіт'});Конструктор — це єдине місце, де можна присвоїти this.state.
Станові оновлення можуть бути асинхронними
React може групувати кілька викликів setState() в одне оновлення для продуктивності.
Оскільки this.props і this.state можуть бути оновлені асинхронно, не варто покладатися на їх значення для обчислення наступного стану.
Наприклад, цей код може не оновити лічильник:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});Щоб виправити це, скористайтеся другою формою setState(), яка приймає функцію, а не об’єкт. Ця функція отримає попередній стан як перший аргумент і значення пропсів безпосередньо в момент оновлення як другий аргумент:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));Вище було використано стрілкову функцію, але це також працює і зі звичайними функціями:
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});Об’єднання оновлень стану
Коли ви викликаєте setState(), React об’єднує об’єкт, який ви надаєте, із поточним станом.
Наприклад, ваш стан може містити кілька незалежних змінних:
constructor(props) {
super(props);
this.state = {
posts: [], comments: [] };
}Тоді ви можете оновлювати їх окремо за допомогою викликів setState():
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts });
});
fetchComments().then(response => {
this.setState({
comments: response.comments });
});
}Злиття є поверхневим, а тому this.setState({comments}) залишає this.state.posts незмінним, але повністю замінює this.state.comments.
Потік даних вниз
Ні батьківські, ні дочірні компоненти не можуть знати, чи є певний компонент становим або безстановим, і вони не повинні піклуватися, чи визначено його як функцію або клас.
Саме тому стан часто називають внутрішнім або інкапсульованим. Він не доступний для будь-якого іншого компоненту, окрім того, який ним володіє і встановлює.
Компонент може передати свій стан вниз у якості пропсів до своїх дочірніх компонентів:
<FormattedDate date={this.state.date} />Компонент FormattedDate отримає date у своїх пропсах і не буде знати, чи він належить стану Clock, чи пропсам Clock, чи був введений вручну:
function FormattedDate(props) {
return <h2>Зараз {props.date.toLocaleTimeString()}.</h2>;
}Це зазвичай називається “зверху вниз” або “односпрямованим” потоком даних. Будь-який стан завжди належить певному компоненту і будь-які дані або UI, отримані з цього стану, можуть впливати лише на компоненти, що знаходяться “нижче” у дереві.
Уявіть дерево компонентів як водоспад пропсів, де стан кожного компонента подібний до додаткового джерела води, який приєднується до нього в довільній точці, але тече вниз.
Щоб показати, що всі компоненти є дійсно ізольованими, ми можемо створити компонент App, який рендерить три <Clock>:
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);Кожен Clock встановлює свій власний таймер і оновлюється самостійно.
У додатках React, наявність або відсутність стану в компонента вважається деталлю реалізації компонента, і може змінюватися з часом. Можна використовувати компоненти без стану всередині компонентів зі станом та навпаки.