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, независимо от того, обладает ли компонент состоянием – состояние является деталью реализации этого компонента и может изменяться со временем. Вы можете использовать компоненты без состояния внутри компонентов, имеющих состояние, и наоборот.