Состояние и жизненный цикл
На этой странице представлены понятия «состояние» (state) и «жизненный цикл» (lifecycle) 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
не обновляет себя каждую секунду автоматически. Хотелось бы спрятать логику, управляющую таймером, внутри самого компонента Clock
.
В идеале мы бы хотели реализовать Clock
таким образом, чтобы компонент сам себя обновлял:
ReactDOM.render(
<Clock />, document.getElementById('root')
);
Для этого добавим так называемое «состояние» (state) в компонент Clock
.
«Состояние» очень похоже на уже знакомые нам пропсы, отличие в том, что состояние контролируется и доступно только конкретному компоненту.
Преобразование функционального компонента в классовый
Давайте преобразуем функциональный компонент Clock
в классовый компонент за 5 шагов:
- Создаём ES6-класс с таким же именем, указываем
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
и обновлять компонент каждую секунду.
Добавим методы жизненного цикла в класс
В приложениях со множеством компонентов очень важно освобождать используемые системные ресурсы, когда компоненты удаляются.
Первоначальный рендеринг компонента в DOM называется «монтирование» (mounting). Нам нужно устанавливать таймер всякий раз, когда это происходит.
Каждый раз когда DOM-узел, созданный компонентом, удаляется, происходит «размонтирование» (unmounting). Чтобы избежать утечки ресурсов, мы будем сбрасывать таймер при каждом «размонтировании».
Объявим специальные методы, которые компонент будет вызывать при монтировании и размонтировании:
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>
);
}
}
Эти методы называются «методами жизненного цикла» (lifecycle methods).
Метод componentDidMount()
запускается после того, как компонент отрендерился в DOM — здесь мы и установим таймер:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }
Обратите внимание, что мы сохраняем ID таймера в this
(this.timerID
).
Поля this.props
и this.state
в классах — особенные, и их устанавливает сам React. Вы можете вручную добавить новые поля, если компоненту нужно хранить дополнительную информацию (например, ID таймера).
Теперь нам осталось сбросить таймер в методе жизненного цикла componentWillUnmount()
:
componentWillUnmount() {
clearInterval(this.timerID); }
Наконец, реализуем метод tick()
. Он запускается таймером каждую секунду и вызывает this.setState()
.
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
должен отображать текущее время, поэтому мы задаём начальное состояниеthis.state
объектом с текущим временем. - React вызывает метод
render()
компонентаClock
. Таким образом React узнаёт, что отобразить на экране. Далее React обновляет DOM так, чтобы он соответствовал выводу рендераClock
. - Как только вывод рендера
Clock
вставлен в DOM, React вызывает метод жизненного циклаcomponentDidMount()
. Внутри него компонентClock
указывает браузеру установить таймер, который будет вызыватьtick()
раз в секунду. - Таймер вызывает
tick()
ежесекундно. Внутриtick()
мы просим React обновить состояние компонента, вызываяsetState()
с текущим временем. React реагирует на изменение состояния и снова запускаетrender()
. На этот разthis.state.date
в методеrender()
содержит новое значение, поэтому React заменит DOM. Таким образом компонентClock
каждую секунду обновляет UI. - Если компонент
Clock
когда-либо удалится из DOM, React вызовет метод жизненного циклаcomponentWillUnmount()
и сбросит таймер.
Как правильно использовать состояние
Важно знать три детали о правильном применении setState()
.
Не изменяйте состояние напрямую
В следующем примере повторного рендера не происходит:
// Неправильно
this.state.comment = 'Привет';
Вместо этого используйте setState()
:
// Правильно
this.setState({comment: 'Привет'});
Конструктор — это единственное место, где вы можете присвоить значение this.state
напрямую.
Обновления состояния могут быть асинхронными
React может сгруппировать несколько вызовов setState()
в одно обновление для улучшения производительности.
Поскольку this.props
и this.state
могут обновляться асинхронно, вы не должны полагаться на их текущее значение для вычисления следующего состояния.
Например, следующий код может не обновить счётчик:
// Неправильно
this.setState({
counter: this.state.counter + this.props.increment,
});
Правильно будет использовать второй вариант вызова setState()
, который принимает функцию, а не объект. Эта функция получит предыдущее состояние в качестве первого аргумента и значения пропсов непосредственно во время обновления в качестве второго аргумента:
// Правильно
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
В данном примере мы использовали стрелочную функцию, но можно использовать и обычные функции:
// Правильно
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
Обновления состояния объединяются
Когда мы вызываем setState()
, React объединит аргумент (новое состояние) c текущим состоянием.
Например, состояние может состоять из нескольких независимых полей:
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
или просто JavaScript-выражения:
function FormattedDate(props) {
return <h2>Сейчас {props.date.toLocaleTimeString()}.</h2>;
}
Это, в общем, называется «нисходящим» («top-down») или «однонаправленным» («unidirectional») потоком данных. Состояние всегда принадлежит определённому компоненту, а любые производные этого состояния могут влиять только на компоненты, находящиеся «ниже» в дереве компонентов.
Если представить иерархию компонентов как водопад пропсов, то состояние каждого компонента похоже на дополнительный источник, который сливается с водопадом в произвольной точке, но также течёт вниз.
Чтобы показать, что все компоненты действительно изолированы, создадим компонент App
, который рендерит три компонента <Clock>
:
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
У каждого компонента Clock
есть собственное состояние таймера, которое обновляется независимо от других компонентов.
В React-приложениях, имеет ли компонент состояние или нет — это внутренняя деталь реализации компонента, которая может меняться со временем. Можно использовать компоненты без состояния в компонентах с состоянием, и наоборот.