State и Lifecycle
Тази страница представя концепцията за state(състояние) и lifecycle(жизнен цикъл) на компонентите в React. Можете да намерите подробна информация за API(програмния интерфейс) на компонент тук.
Спомнете си за примера с часовника в един от предишните раздели. В Рендериране на елементи, до сега научихме само един начин да актуализираме потребителския интерфейс. Извикваме ReactDOM.render()
за да актуализираме резултата от рендерирането:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render( element, document.getElementById('root') );}
setInterval(tick, 1000);
В този раздел, ще научим как да променим Clock
компонента, така че да бъде преизползваем и капсулиран. Ще създаде собствен таймер и ще го актуализира на всяка секунда.
Можем да започнем с капсулирането на това как часовника е визуализиран:
function Clock(props) {
return (
<div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> );
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />, document.getElementById('root')
);
}
setInterval(tick, 1000);
Въпреки това, в примера липсва едно съществено изискване: Clock
създава таймер и актуализира потребителският интерфейс всяка секунда, но това трябва да бъде имплементирано в самия Clock
.
В идеалния случай ние бихме искали да напишем това само веднъж и Clock
да се актуализира сам:
ReactDOM.render(
<Clock />, document.getElementById('root')
);
За да имплементираме това, ще трябва да добавим state
към компонента Clock
.
State
e подобен на props
, но е вътрешен за компонента и се контролира напълно от самия него.
Както споменахме в предишния раздел компонентите, които са дефинирани като класове имат допълнителни свойства. Локалния state е пример за това: свойство което имат само класовете.
Преобразуване от Функция към Клас
Можете да преобразувате Clock
, който е “компонент функция” на клас в пет стъпки:
- Създаване на ES6 клас, със същото име, което наследява
React.Component
. - Добавяне на празен метод наречен
render()
. - Преместване на тялото на функцията вътре в
render()
метода. - Заменяне на
props
сthis.props
вътре в тялото наrender()
метода. - Изтриване на останалата празна част от функцията.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Clock
сега е дефиниран като клас вместо като функция.
Методът render
ще бъде извикван всеки път когато се актуализара, но докато <Clock />
се рендерира към същия DOM елемент, само една единствена инстанция на класа Clock
ще бъде използвана. Това ни позволява да използваме допълнителни свойства като локален state и “lifecycle методи”.
Добавяне на локален State към Клас
Ще преместим date
от props в state с три стъпки:
- Заменяме
this.props.date
сthis.state.date
вrender()
метода:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
- Добавяме в класа constructor метод, който ще присвои
this.state
за първи път:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}; }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Забележете как подаваме props
към базовия constructor метод:
constructor(props) {
super(props); this.state = {date: new Date()};
}
Компонентите които са дефинирани като класове трябва винаги да извикват базовия constructor с props
.
- Изтриваме
date
prop от<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>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
ReactDOM.render(
<Clock />, document.getElementById('root')
);
В следващата стъпка, ще направим, така че Clock
да настройва сам свой собствен таймер и да го актуализа сам на всяка секунда.
Добавяне на Lifecycle методи към Клас
В уеб приложения с много компоненти е много важно да освобождаваме ресурсите заети от тях, когато те бъдат премахнати.
Искаме да настроиваме таймер всеки път, когато Clock
е рендериран в DOM дървото за първи път. Това се нарича “mounting”(закачане) в React.
Също така искаме да зачистваме таймера всеки път, когато Clock
бъде премахнат от DOM дървото. Това се нарича “unmounting”(разкачане) в React.
Можем да декларираме специални методи в компонент клас, за да изпълним определен код, когато компонента се закача или разкача:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() { }
componentWillUnmount() { }
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Тези методи се наричат “lifecycle методи”.
Метода componentDidMount()
се изпълнява след като компонента се рендерира в DOM дървото. Това е добро място, на което да настроим таймера:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }
Забележете че запазваме ID-то на таймера директно в this
(this.timerID
).
Докато this.props
се дефинират от самия React, this.state
има специална употреба. Имаме възможността да добавяме допълнителни полета към класа ръчно ако имаме нужда да съхраняваме неща, които не участват в потока от данни (например ID-то на таймера).
Ще зачистим таймера в lifecycle метода componentWillUnmount()
:
componentWillUnmount() {
clearInterval(this.timerID); }
И накрая ще имплементираме метода tick()
, който компонента Clock
ще изпълнява всяка секунда.
Той ще използва this.setState()
, за да актуализира локалния state на компонента:
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>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
Сега вече часовникът ще се актуализира на всяка секунда.
Нека накратко да резюмираме какво се случва и реда, в който методите се изпълняват:
- Когато
<Clock />
се подаде наReactDOM.render()
, React извиква конструктор метода на компонентаClock
. Тъй катоClock
трябва да визуализира текущото време, той инициализираthis.state
с обект съдържащ текущото време. В по-късен етап ще актуализирамеthis.state
. - React извиква метода
render()
на компонентаClock
. По този начин React разбира, какво трябва да бъде визуализирано на екрана. След това React актуализира DOM дървото, така че да съответства на резултата от рендерирането наClock
. - Когато резултата на
Clock
бъде добавен към DOM дървото, React извиква lifecycle методаcomponentDidMount()
. Вътре в него, компонентаClock
казва на browser-а да настройва таймера да извиква методаtick()
на всяка секунда. - Всяка секунда browser-а извиква метода
tick()
. Вътре в него, компонентаClock
насрочва промяна в потребителският интерфейс като извиква методаsetState()
с обект съдържащ текущото време. КогатоsetState()
бъде извикан, React разбира че state-а е променен, и извикваrender()
метода отново за да разбере какво трябва да се визуализира на екрана. Този път,this.state.date
намиращ се вrender()
метода ще бъде различен, и така резултата от render метода ще съдържа актуализираното време. Съответно React ще актуализира DOM дървото. - Ако компонента
Clock
бъде изтрит от DOM дървото, React ще извика lifecycle методаcomponentWillUnmount()
и така таймерът ще спре.
Как да използваме правилно State
Има три неща които трябва да знаете относно setState()
.
State-а не трябва да бъде променян директно
Следния пример няма да рендерира компонента наново:
// Wrong
this.state.comment = 'Hello';
Вместо това използвайте setState()
:
// Correct
this.setState({comment: 'Hello'});
Единственото място в което this.state
може да бъде присвоен е в конструктора.
Актуализирането на State-а може да бъде асинхронно
React може да групира множество извиквания на setState()
в една единствена актуализация, за да подобри производителността.
Тъй като this.props
и this.state
може да бъдат актуализирани асинхронно, не трябва да разчитате на текущите им стойности, когато пресмятате бъдещият state.
Следният примерен код, може да не успее да актуализира таймера коректно:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
За да поправите това, използвайте друга форма на setState()
, която приема като аргумент функция вместо обект. Тази функция ще получи предишният state като първи аргумент и props като втори аргумент в момента в който е имало актуализация.
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
В горния пример използваме arrow функция, но също така може да бъде използвана и обикновенна такава:
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
Промените на State-а се сливат
Когато извикате setState()
, React слива обекта, който подавате в настоящия state.
Например, state-а може да съдържа няколко независими променливи:
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
.
Потокът на данни е от родител към деца
Нито родителя, нито неговите дъщерни компоненти могат да знаят дали даден компонент е “stateful” или “stateless” и също така не знаят дали той е дефиниран като функция или клас.
Поради тази причина често state-а е наричан локален или капсулиран. Той не е достъпен от никой друг компонент освен този, в който е дефиниран и променян.
Компонентът може да подаде своят state като prop към компонентите, които са му деца.
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
Това важи също за дефинирани от потребителя компоненти:
<FormattedDate date={this.state.date} />
Компонентът FormattedDate
ще получи date
в своите props и няма да знае, дали е дошъл от state-а на Clock
, от props на Clock
или е подаден ръчно.
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
Този поток от данни често е наричан “отгоре-надолу” или “еднопосочен”. Всеки state е част от един специфичен компонент и всякакви данни или потребителски интерфейс получени като резултат от state-а могат да афектира само върху компонентите, които са “надолу” в дървото от компоненти.
Представете си дървото от компоненти като водопад от props, като state-а на всеки компонент е допълнителен водоизточник, който се присъединява в даден момент, но също така изтича надолу.
За да видим, че всички компоненти са наистина изолирани, можем да създадем App
компонент, който рендерира три <Clock>
компонента.
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Всеки Clock
създава свой таймер и го актуализира независимо от другите.
В React, дали даден компонент е “stateful” или “stateless” се счита за детайл на самата имплементация на компонента и тя може да се променя във времето. Можете да използвате “stateless” компоненти вътре в “stateful” компоненти и обратното.