Состояние и жизненный цикл
На этой странице представлена концепция состояния и жизненного цикла в компоненте 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')
);
Для реализации этого, нам нужно добавить «состояние» к компоненту Clock
.
Состояние похоже на свойство, но оно является закрытым и полностью контролируется компонентом.
Мы упоминали ранее, что компоненты, определённые как классы, имеют некоторые дополнительные возможности. Локальное состояние — это как раз одно из них: эта возможность доступна только классам.
Преобразование функции в класс
Преобразовать функциональный компонент, такой как Clock
, в классовый компонент можно за пять шагов:
-
Создать 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.props.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.props.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.props.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.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
Эти методы называются “хуками (методами) жизненного цикла”.
Хук componentDidMount()
запускается после того, как вывод компонента отрисован в DOM. Это хорошее место для установки таймера:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
Обратите внимание, что мы сохраняем идентификатор таймера в this
.
Хотя 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.props.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
. -
Когда в DOM вставлен вывод
Clock
, React вызывает хук жизненного циклаcomponentDidMount()
. Внутри него компонентClock
указывает браузеру настроить таймер для вызова методаtick()
компонента один раз в секунду. -
Каждую секунду браузер вызывает метод
tick()
. Внутри него компонентClock
планирует обновление пользовательского интерфейса, вызываяsetState()
с объектом, содержащим текущее время. Благодаря вызовуsetState()
React знает, что состояние изменилось, и снова вызывает методrender()
, чтобы узнать, что должно отображаться на экране. На этот разthis.state.date
в методеrender()
будет другим, и поэтому вывод отрисованного компонента будет включать обновлённое время. React обновляет DOM соответствующим образом. -
Если компонент
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
.
Однонаправленный поток данных
Ни родительский, ни дочерний компоненты не могут знать, является ли какой-либо компонент с состоянием или без него, и им не важно, определен ли он как функция или класс.
Вот почему состояние часто называют локальным или инкапсулированным. Оно недоступно для любого компонента, за исключением того, который владеет и устанавливает его.
Компонент может передать своё состояние вниз по дереву компонентов в виде свойства его дочерних компонентов:
<h2>Сейчас {this.state.date.toLocaleTimeString()}.</h2>
Это также работает для пользовательских компонентов:
<FormattedDate date={this.state.date} />
Компонент FormattedDate
получил бы date
в своих свойствах и не знал бы, пришло ли оно из состояния Clock
, из свойств Clock
или было передано напрямую:
function FormattedDate(props) {
return <h2>Сейчас {props.date.toLocaleTimeString()}.</h2>;
}
Это обычно называют потоком данных «сверху вниз» или «однонаправленным потоком данных». Любое состояние всегда принадлежит определённому компоненту, а любые данные или пользовательский интерфейс, полученные из этого состояния, могут влиять только на компоненты, находящиеся «ниже» в их дереве компонентов.
Если вы представляете дерево компонентов как водопад свойств, состояние каждого компонента похоже на дополнительный источник воды, который соединяется с водопадом в произвольной точке, но также течёт вниз.
Чтобы показать, что все компоненты действительно изолированы, мы можем создать компонент App
, который отрисовывает три компонента <Clock>
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
У каждого компонента Clock
есть собственное состояние таймера, которое обновляется независимо от других компонентов .
В React-приложениях, независимо от того, является ли компонент, имеющим состояние или нет, — это рассматривается как деталь реализации компонента, которая может измениться со временем. Вы можете использовать компоненты без состояния в компонентах с состоянием, и наоборот.