2.6 Состояние и жизненный цикл

Рассмотрим пример тикающих часов из предыдущего раздела.

До сих пор мы изучили только один способ обновления 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);
  

Посмотреть в CodePen


В этом разделе мы изучим как сделать компонент 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);
    

Посмотреть в CodePen


Тем не менее, он упускает ключевое требование: установка таймера и обновление UI каждую секунду должны быть деталью реализации Timer.

В идеале, нам необходимо написать следующий код с компонентом Timer, обновляющим самого себя:


Код
        
  ReactDOM.render(<Timer value={total}/>, document.getElementById('root'));
    

Для того, чтобы это реализовать, нам необходимо добавить «состояние» к компоненту Timer.

Состояние подобно свойствам props, но является приватным и полностью контролируется компонентом.



Ранее мы упоминали, что компоненты, определённые как классы, имеют некоторые дополнительные возможности. Локальное состояние является возможностью, доступной только для классов.


2.6.1 Преобразование функций в классы

Мы можем преобразовать функциональный компонент Timer в класс в пять шагов:

  1. Создать ES6-класс с тем же самым именем, который расширяет React.Component.
  2. Добавить в него единственный пустой метод под названием render().
  3. Поместить тело функции в метод render().
  4. Заменить props на this.props в теле метода render().
  5. Удалить оставшееся пустое определение функции


Код
        
  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>
      );
    }
  }
    

Посмотреть в CodePen


Теперь компонент Timer определен как класс, а не функция.

Это позволяет нам использовать дополнительные возможности, такие как локальное состояние и методы жизненного цикла.


2.6.2 Добавление локального состояния в класс

Мы переместим date из props в состояние в три шага.

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'));
    

Посмотреть в CodePen


Далее мы сделаем так, что компонент Timer будет устанавливать таймер и обновлять себя каждую секунду.


2.6.3 Добавление методов жизненного цикла в класс

В приложениях с множеством компонентов очень важно высвобождать ресурсы, занятые компонентами, когда они уничтожаются.

Нам необходимо устанавливать таймер каждый раз, когда Timer отрисовывается в DOM в первый раз. В React это называется «монтированием/монтажом».

Также нам нужно очищать этот таймер, каждый раз когда DOM, созданный компонентом 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.

В то время как this.props самостоятельно устанавливаются React-ом и 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'));
    

Посмотреть в CodePen


Теперь таймер обновляется каждый установленный промежуток времени.



Теперь давайте прорезюмируем, что произошло и порядок, в котором вызываются методы:

  1. Когда <Timer/> передан в ReactDOM.render(), React вызывает конструктор компонента Timer. Как только Timer нуждается в отображении текущего значения, он инициализирует this.state с объектом, включающим текущее значение таймера. Позже мы обновим это состояние.
  2. Далее React вызывает метод render() компонента Timer. Это то, как React понимает, что должно быть отображено на экране. Далее React обновляет DOM, в соответствии с результатом отрисовки Timer.
  3. Когда результат отрисовки Timer вставлен в DOM, React вызывает метод componentDidMount() жизненного цикла. Внутри него компонент Timer обращается к браузеру для установки таймера, чтобы вызывать increment() раз в секунду.
  4. Каждую секунду браузер вызывает метод increment(). Внутри него Timer компонент планирует обновление UI с помощью вызова setState() с объектом, содержащим текущее время. Благодаря вызову setState(), React знает, что состояние изменилось, и вызывает метод render() снова, чтобы узнать, что должно быть на экране. Значение this.state.value в методе render() будет отличаться, поэтому результат отрисовки будет содержать обновленное значение таймера. React обновляет DOM соответственно.
  5. Если компонент Timer в какой-то момент удалён из DOM, React вызывает метод componentWillUnmount() жизненного цикла, из-за чего таймер останавливается.


2.6.4 Корректное обновление состояния

Необходимо знать три вещи о setState().


2.6.4.1 Не модифицируйте состояние напрямую

К примеру, это не перерисовываемый компонент:


Код
        
  // Неправильно :(
  this.state.message = 'Привет, Мир!';
    

Вместо этого используйте метод setState():


Код
        
  // Правильно :)
  this.setState({message: 'Привет, Мир!'})
    


Внимание!

Вы можете назначить this.state только в конструкторе!


2.6.4.2 Обновления состояния могут быть асинхронными

React может собирать множественные вызовы setState() в единое обновление в целях производительности.

Так как 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 Обновления состояния мерджатся (merge)

Когда вы вызываете 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.


2.6.5 Нисходящий поток данных

Ни родительский, ни дочерний компоненты не могут знать, обладает ли определенный компонент состоянием или нет. Также они не заботятся о том, определен ли он как функция или класс.

Вот почему состояние часто называется локальным или инкапсулированным. Оно недоступно для какого-либо компонента, за исключением того, который им владеет и устанавливает.

Компонент может решить передать это состояние вниз как свойства 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>
    );
  }
    

Посмотреть в CodePen


Это принято называть «сверху-вниз», «нисходящим» или однонаправленным потоком данных. Любое состояние всегда находится во владении какого-либо компонента, и любые данные или UI, производные от этого состояния могут затрагивать только компоненты «ниже» их в дереве иерархии.

Если вы представляете дерево компонентов как «водопад» свойств, то состояние каждого компонента является подобием дополнительного источника воды, который соединяется с водопадом в произвольной точке и также течет вниз.

Для того, чтобы показать, что все компоненты действительно изолированы, мы можем создать компонент Application, который отрисовывает <Timer/>:


Код
        
  function Application() {
    return (
      <p>
        <Timer/>
        <Timer/>
        <Timer/>
      </p>
    );
  }

  ReactDOM.render(<Application/>, document.getElementById('root'));
    

Посмотреть в CodePen


Каждый компонент <Timer/> устанавливает своё собственное значение и обновляется независимо.

В приложениях React, независимо от того, обладает ли компонент состоянием или нет – оно рассматривается как деталь реализации этого компонента, который может изменяться со временем. Вы можете использовать компоненты без состояния внутри компонентов, имеющих состояние, и наоборот.