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

Посмотреть в 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, однако является приватным и полностью контролируется компонентом.



Раньше состоянием могли обладать только компоненты-классы. Однако с появлением хуков состоянием могут обладать и компоненты-функции.




Мы можем преобразовать компонент-функцию 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 определён как класс, а не как функция.




Давайте переместим 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'));
    

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


Далее мы сделаем так, что компонент 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'));
    

Посмотреть в 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() жизненного цикла, из-за чего таймер останавливается.




О 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>
    );
  }
    

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


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

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

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


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

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

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


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

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