2.6 Состояние и жизненный цикл
В этом разделе мы расскажем о таких важных концепциях, как состояние и жизненный цикл компонента React. Более подробный API компонента вы можете найти здесь.
Рассмотрим, упомянутый ранее, пример тикающих часов.
Пока что мы знаем только один способ обновления UI.
Мы вызываем ReactDOM.render(), чтобы изменить результат отрисовки:
const INTERVAL = 100;
let total = 0;
function increment() {
total++;
const element = (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(total/INTERVAL/60/60)} : </span>
<span>{Math.round(total/INTERVAL/60)} : </span>
<span>{Math.round(total/INTERVAL)} . </span>
<span>{total % INTERVAL}</span>
</p>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(increment, 1000/INTERVAL);
В этом разделе мы сделаем компонент Timer по-настоящему переиспользуемым и
инкапсулированным. Он сначала установит собственный таймер, а затем станет периодически обновляться
через определенный промежуток времени.
Давайте начнём с инкапсуляции кода в компонент Timer:
const INTERVAL = 100;
let total = 0;
function Timer(props) {
const value = props.value;
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
function increment() {
total++;
ReactDOM.render(<Timer value={total}/>, document.getElementById('root'));
}
setInterval(increment, 1000/INTERVAL);
Прекрасно! Однако мы пока не учли ключевое требование: установка таймера и обновление UI
каждую секунду должны быть деталью реализации Timer.
В идеале, нам необходимо спроектировать самообновляющийся компонент Timer так, чтобы код, который его
использует имел следующий вид:
ReactDOM.render(<Timer value={total}/>, document.getElementById('root'));
Чтобы этого добиться, к компоненту Timer нужно добавить состояние.
Состояние похоже на свойства props, однако является приватным и полностью контролируется компонентом.
Раньше состоянием могли обладать только компоненты-классы. Однако с появлением хуков состоянием могут обладать и компоненты-функции.
Мы можем преобразовать компонент-функцию Timer в класс за пять шагов:
- Создать одноимённый ES6-класс, который расширяет
React.Component. - Добавить в него единственный пустой метод под названием
render(). - Поместить тело функции в метод
render(). - Заменить
propsнаthis.propsв теле методаrender(). - Удалить оставшееся пустое определение функции
class Timer extends React.Component {
render() {
const value = this.props.value
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
}
Теперь компонент Timer определён как класс, а не как функция.
Давайте переместим date из props в state в три этапа.
1. Заменим this.props.value на this.state.value в методе render():
class Timer extends React.Component {
render() {
const value = this.state.value
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
}
2. Добавим конструктор класса, который устанавливает начальное состояние this.state:
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {value: 0};
}
render() {
const value = this.state.value
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
}
Обратите внимание на то, как мы передаем свойства props в базовый конструктор:
constructor(props) {
super(props);
this.state = {value: 0};
}
Компоненты-классы должны всегда вызывать базовый конструктор с props.
3. Удаляем свойство value из <Timer /> элемента:
ReactDOM.render(<Timer />, document.getElementById('root'));
Позже мы добавим код таймера обратно в сам компонент.
Результат будет выглядеть следующим образом:
const INTERVAL = 100;
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {value: 0};
}
render() {
const value = this.state.value
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
}
ReactDOM.render(<Timer/>, document.getElementById('root'));
Далее мы сделаем так, что компонент Timer будет
устанавливать таймер и обновлять себя каждую секунду.
При старте приложения React, компонент Timer будет впервые отрисован в DOM.
В React это называется монтированием/монтажом компонента.
Обратная процедура, при которой DOM, созданный компонентом Timer,
удаляется, называется демонтированием/демонтажём.
После каждого монтирования Timer ему нужно устанавливать
таймер, чтобы периодически себя обновлять.
Однако в приложениях с множеством компонентов очень важно высвобождать ресурсы, занятые компонентами, когда они уничтожаются, чтобы избежать утечек памяти.
В нашем случае таймер - это ресурс и нам нужно очищать его перед каждым демонтированием компонента.
React позволяет объявить в компоненте-классе специальные методы, чтобы запускать определенный код, когда компонент монтируется или демонтируется:
const INTERVAL = 100;
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {value: 0};
}
componentDidMount() {
}
componentWillUnmount() {
}
В документации эти методы называются «lifecycle hooks». Мы же для простоты будем называть их методами жизненного цикла (ЖЦ).
Метод componentDidMount() срабатывает после того, как
компонент был впервые отрисован в DOM - монтирован. Это отличное место, чтобы установить таймер:
componentDidMount() {
this.timerID = setInterval(() => this.increment(), 1000/INTERVAL);
}
Обратите внимание, как мы сохраняем ID таймера прямо в this.
В то время как React самостоятельно устанавливает свойства this.props, а
this.state имеет определенное значение, вы можете вручную добавить в класс
дополнительные поля, если вам нужно хранить что-то, что не используется для результата отрисовки.
Если вы не используете что-то в render(), оно не должно
находиться в состоянии state.
Мы будем очищать таймер в методе жизненного цикла componentWillUnmount():
componentWillUnmount() {
clearInterval(this.timerID);
}
Далее, мы реализуем метод increment(), который будет выполняться каждую секунду.
Он будет использовать this.setState(), чтобы планировать обновления
в локальном состоянии компонента:
const INTERVAL = 100;
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {value: 0};
}
increment(){
this.setState({value: this.state.value + 1});
}
componentDidMount() {
this.timerID = setInterval(() => this.increment(), 1000/INTERVAL);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
render() {
const value = this.state.value
return (
<div>
<p>Таймер:</p>
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
</div>
);
}
}
ReactDOM.render(<Timer/>, document.getElementById('root'));
Теперь компонент постоянно обновляется через установленный промежуток времени.
Давайте подытожим всё, что произошло, а также порядок, в котором вызываются методы:
-
Когда
<Timer/>передан вReactDOM.render(), React вызывает конструктор компонентаTimer. Как толькоTimerнуждается в отображении текущего значения, он инициализируетthis.stateобъектом, включающим текущее значение таймера. Позже мы обновим это состояние. -
Далее React вызывает метод
render()компонентаTimer. Возвращаемый им результат - это то, как React понимает, что должно быть отображено на экране. Далее React обновляет DOM, в соответствии с результатом отрисовкиTimer. -
Когда результат отрисовки
Timerвставлен в DOM, React вызывает методcomponentDidMount()жизненного цикла. Внутри него компонентTimerобращается к браузеру для установки таймера, чтобы вызыватьincrement()раз в секунду. -
Браузер вызывает метод
increment()каждую секунду. Внутри него компонентTimerпланирует обновление UI с помощью вызоваsetState()с объектом, содержащим текущее время. Благодаря вызовуsetState(), React знает, что состояние изменилось, и вызывает методrender()снова, чтобы узнать, что должно быть на экране. Значениеthis.state.valueв методеrender()будет отличаться, поэтому результат отрисовки будет содержать обновленное значение таймера. React обновляет DOM соответственно. -
Если компонент
Timerв какой-то момент удалён из DOM, React вызывает методcomponentWillUnmount()жизненного цикла, из-за чего таймер останавливается.
О setState() нужно знать три вещи.
2.6.4.1 Не модифицируйте состояние напрямую
К примеру, этот компонент перерисовываться не будет:
// Неправильно :(
this.state.message = 'Привет, Мир!';
Для корректной модификации состояния компонента используйте метод setState():
// Правильно :)
this.setState({message: 'Привет, Мир!'})
Внимание!
Вы можете установитьthis.state только в конструкторе!
2.6.4.2 Обновления состояния могут быть асинхронными
React может собирать последовательность вызовов setState() в единое обновление
в целях повышения производительности.
Так как React может обновлять this.props и this.state асинхронно,
вы не должны полагаться на их значения для вычисления следующего состояния.
К примеру, такой код может не обновить температуру:
// Неправильно :(
this.setState({temperature: this.state.temperature + this.props.delta});
Чтобы это исправить, используйте следующую форму метода setState(), который
принимает функцию, вместо объекта. Эта функция будет принимать предыдущее состояние как первый
аргумент и свойства в момент обновления как второй аргумент.
// Правильно :)
this.setState((prevState, props) => ({
temperature: prevState.temperature + props.delta
}));
Мы использовали стрелочную функцию, но можно использовать и обычные функции:
// Правильно :)
this.setState(function(prevState, props) {
return {temperature: prevState.temperature + props.delta};
});
2.6.4.3 Обновления состояния объединяются
Когда вы вызываете setState(), React производит
объединение(слияние) текущего состояния и объекта, который вы предоставили.
Например, состояние вашего компонента может содержать множество независимых переменных:
constructor(props) {
super(props);
this.state = {
permissions: [],
users: []
};
}
Далее вы можете обновить их независимо с помощью отдельных вызовов setState():
componentDidMount() {
fetchPermissions().then(response => {
this.setState({
permissions: response.permissions
});
});
fetchUsers().then(response => {
this.setState({
users: response.users
});
});
}
Объединение неглубокое, поэтому this.setState({users}) оставляет this.state.permissions
нетронутым, но полностью заменяет this.state.users.
О наличии состояния у определенного компонента не могут знать ни его родительский, ни дочерний компоненты. Более того - они даже не знают, каким образом определен компонент: с помощью функции или класса.
Вот почему состояние часто называют локальным или инкапсулированным. Оно недоступно для какого-либо компонента, за исключением того, который им владеет и устанавливает.
Компонент может решить передать это состояние вниз как свойства
props своим дочерним компонентам:
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
Таким же образом это работает и для пользовательских компонентов:
<ClockFace value={this.state.value}/>
Компонент ClockFace хотел бы получать значение value в
своих свойствах. Ему незачем знать откуда оно пришло: из состояния компонента Timer,
из свойств компонента Timer или было указано вручную:
function ClockFace(props){
const value = props.value;
return (
<p>
<span>{Math.round(value/INTERVAL/60/60)} : </span>
<span>{Math.round(value/INTERVAL/60)} : </span>
<span>{Math.round(value/INTERVAL)} . </span>
<span>{value % INTERVAL}</span>
</p>
);
}
Это принято называть «сверху-вниз», «нисходящим» или однонаправленным потоком данных. Любое состояние всегда находится во владении какого-либо компонента. Любые данные или UI, производные от этого состояния могут передаваться только в компоненты «ниже» их в дереве иерархии.
Если представить дерево компонентов как «водопад» свойств, то состояние каждого компонента является подобием дополнительного источника воды, который соединяется с водопадом в произвольной точке и также течет вниз.
Чтобы показать, что все компоненты действительно изолированы,
мы можем создать компонент Application, который отрисовывает <Timer/>:
function Application() {
return (
<p>
<Timer/>
<Timer/>
<Timer/>
</p>
);
}
ReactDOM.render(<Application/>, document.getElementById('root'));
Каждый компонент <Timer/> устанавливает своё собственное значение и
обновляется независимо.
В приложениях React, независимо от того, обладает ли компонент состоянием – состояние является деталью реализации этого компонента и может изменяться со временем. Вы можете использовать компоненты без состояния внутри компонентов, имеющих состояние, и наоборот.
