Состояние и жизненный цикл в React
Все React-компоненты, рассматриваемые до сих пор, были статическими и не поддерживали динамических возможностей. Настало время сделать их динамическими.
Рассмотрим пример с часами из предыдущего раздела документации по React (Обновление элементов в React).
До сих пор мы узнали только один из способов обновления пользовательского интерфейса.
Мы вызываем ReactDOM.render()
, чтобы изменить выводимые данные:
function tick() {
const element = (
<div>
<h1>Привет, мир!</h1>
<h2>Сейчас {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);}
setInterval(tick, 1000);
Смотреть пример в CodePen.
В этом разделе мы узнаем как сделать компонент 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);
Смотреть пример в CodePen.
However, it misses a crucial requirement: the fact that the Clock sets up a timer and updates the UI every second should be an implementation detail of the 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>
);
}
}
Смотреть пример в CodePen.
В настоящее время Clock определяется как класс, а не как функция. Это позволит нам использовать локальное состояние и привязки жизненного цикла.
Добавление локального состояния к классу
Мы переместим date из свойства в состояние за три этапа:
1) Замените this.props.date
на this.state.date
в методе render()
:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Привет, мир!</h1>
<h2>Сейчас {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
2) Добавтье класс constructor, которому присваивается начальное 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.
3) Удалите свойство 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')
);
Смотреть пример в CodePen.
Далее, мы сделаем чтобы Clock создал свой собственный таймер и самообновлялся каждую секунду.
Добавление жизненного цикла в класс
В приложениях с большим количеством компонентов очень важно высвободить ресурсы, используемые компонентами, когда они будут уничтожены.
Мы хотим устанавливать таймер всякий раз, когда Clock выводится в DOM впервые. Это называется "монтированием" в React. Мы также хотим удалять таймер всякий раз, когда DOM, произведенный от Clock удаляется. Это называется "размонтированием" в React.
We can declare special methods on the component class to run some code when a component mounts and unmounts:
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 hooks".
componentDidMount()
выполнятся после того, как результат выполнения компонента выводится в DOM. Это отличное место для таймера:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
Обратите внимание, как мы сохраняем ID таймера прямо в this
.
В то время как React устанавливает this.props
, а this.state
имеет особое значение, вы можете добавить дополнительные поля к классу, если вам нужно хранить что-то, что не используется для визуального вывода. Если вы не используете что-то в render()
, it shouldn't be in the state. We will tear down the timer in the componentWillUnmount()
lifecycle hook:
componentWillUnmount() {
clearInterval(this.timerID);
}
И, наконец, создадим метод tick()
, который выполняется каждую секунду. Он будет использовать this.setState()
to schedule updates to the component local 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')
);
Смотреть пример в CodePen. Теперь часы тикают каждую секунду.
Давайте быстро резюмируем то, что происходит и порядок, в котором вызываются методы:
1) Когда <Clock />
передается в ReactDOM.render()
, React вызывает конструктор из компонента Clock. Поскольку Clock необходимо отобразить текущее время, он инициализирует this.state
с объектом, включающим текущее время. Позднее мы будем обновлять это состояние.
2) React then calls the Clock
component's render()
method. Вот как React узнает, что должно быть на экране. Затем React обновляет DOM, to match the Clock
's render output.
3) Когда Clock
output is inserted in the DOM, React вызывает componentDidMount()
lifecycle hook. Внутри него компонент Clock
asks the browser to set up a timer to call tick()
один раз в секунду.
4) Каждую секунду браузер вызывает метод tick()
. Inside it, the Clock
component schedules a UI update by calling setState()
with an object containing the current time. Thanks to the setState()
call, React knows the state has changed, and calls render()
method again to learn what should be on the screen. This time, this.state.date
in the render()
method will be different, and so the render output will include the updated time. React updates the DOM accordingly.
5) Если компонент Clock
никогда не удаляется из DOM, React вызывает componentWillUnmount()
lifecycle hook so the timer is stopped.
Правильное использование состояния
Есть три вещи, которые вы должны знать о setState()
:
1. Не изменяйте состояние напрямую
Например, так перерисовать компонент не получится:
// Неправильно
this.state.comment = 'Привет';
Вместо этого используйте setState()
:
// Правильно
this.setState({comment: 'Привет'});
Единственное место, где можно назначить this.state
- это конструктор.
2. Обновления состояния могут быть асинхронными
React может упаковать множественные вызовы setState()
в a single update for performance. Поскольку this.props
и this.state
могут быть обновлены асинхронно, вы не должны полагаться на их значения для вычисления следующего состояния. Например, этот код может не обновлять счетчик:
// Неправильно
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState()
that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Правильно
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
We used an arrow function above, but it also works with regular functions:
// Правильно
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
3. Обновления состояния объединены
Когда вы вызываете setState()
, React объединяет выбранный вами объект с текущим состоянием. Например, ваше состояние может содержать несколько независимых переменных:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
Then you can update them independently with separate setState()
calls:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
The merging is shallow, so this.setState({comments})
leaves this.state.posts
intact, but completely replaces this.state.comments
.
The Data Flows Down
Neither parent nor child components can know if a certain component is stateful or stateless, and they shouldn't care whether it is defined as a function or a class.
This is why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it.
A component may choose to pass its state down as props to its child components:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
This also works for user-defined components:
<FormattedDate date={this.state.date} />
The FormattedDate
component would receive the date
in its props and wouldn't know whether it came from the Clock
's state, the props, or was typed by hand:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
Смотреть пример в CodePen.
This is commonly called a "top-down" or "unidirectional" data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components "below" them in the tree.
If you imagine a component tree as a waterfall of props, each component's state is like an additional water source that joins it at an arbitrary point but also flows down.
To show that all components are truly isolated, we can create an App
component that renders three <Clock>
s:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Смотреть пример в CodePen.
Each Clock
sets up its own timer and updates independently.
In React apps, whether a component is stateful or stateless is considered an implementation detail of the component that may change over time. You can use stateless components inside stateful components, and vice versa.