Состояния и жизненный цикл (State and Lifecycle)

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

К этому моменту мы узнали один из способов обновления пользовательского интерфейса.

Мы вызываем ReactDOM.render() для изменения уже выполненного вывода:

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Попробовать на CodePen.

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

Мы можем начать инкапсуляцию часов:

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Попробовать на CodePen.

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

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

 

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

Для реализации этого нам нужно добавить «состояние» в компонент Clock .

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

 

Мы уже упоминали ранее, что компоненты, определенные как классы имеют некоторые дополнительные функции. Локальное состояние является именно доп. функцией: функцией доступной только для классов.

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

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

  1. Создайте ES6 класс с некоторым именем, который расширяет React.Component.
  2. Добавьте пустой метод с названием render().
  3. Переместите тело функции в метод  render().
  4. Замените props на this.props в теле render().
  5. Удалите предыдущее пустое описание функции.
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Попробовать на CodePen.

Clock сейчас определён как класс, а не функция.

Это позволяет нам использовать дополнительные возможности, такие как локальное состояние и жизненный цикл крюк (hooks).

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

Мы будем перемещать date от свойства к состоянию в три этапа:

1) Заменим this.props.date на this.state.date в методе render():

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

2) Добавим конструктор класс который инициализирует this.state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {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>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

Попробовать на CodePen.

Далее, мы сделаем Clock, настроив свой собственный таймер с ежесекундным обновлением.

Добавление классу методов жизненного цикла (Lifecycle Methods)

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

Мы хотим, чтобы установка таймера происходила единожды при первой визуализации  в 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>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Эти методы называются  «lifecycle hooks» (дословно «крючки жизненного цикла», т.е. функциями, цепляющимися к определённым моментам жизни компонента; кстати, англицизм хук используется в русском языке, потому тут и далее будем его использовать).

Хук componentDidMount() запускается после выводе компоненты через отрисовку в DOM. Это хорошее место для установки таймера:

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

Обратите внимание, как мы сохранить ID таймера  в this.

Пока this.props устанавливается React`ом и this.state имеет особое значение, вы можете добавить дополнительные поля классу вручную, если вам нужно хранить что-то, что не используется для визуального вывода.

Если вы не используете что-то в render(), это не должно находится в состоянии.

Мы будем удалять таймеры в хуке  componentWillUnmount():

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

И, наконец, мы реализуем метод tick(), который будет срабатывать каждую секунду.

Он будет использовать 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>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. Так как часам необходимо отображать текущее время, инициализирует  объект this.statewith, включающий текущее время. Позже мы будем обновлять это состояние.

2) Затем React вызывает метод render() компонента Clock. Так React узнает, что должно быть отображено на экране. Затем React обновляет DOM, чтобы соответствовать отрисовке Clock .

3) Когда вывод Clock вставляется в DOM, React вызывает хук componentDidMount(). Внутри него компонент Clock запрашивает браузер об установке таймера для вызова tick() один раз в секунду.

4) Каждую секунду браузер вызывает метод tick(). Компоненты обновляются с той же частотой путём вызова  setState () с объектом, содержащим текущее время. Благодаря вызову setState (), React знает об изменении состояния, и вызывает метод render()  еще раз, чтобы узнать, что должно быть на экране. На этот раз, this.state.date в методе render() будет отличаться, и поэтому визуализация будет включать в себя скорректированное время. React обновит DOM-соответствующим образом.

4) Каждую секунду браузер вызывает метод tick(). Внутри него, компонент Clock планирует обновление UI вызовом setState() с вызовом объекта содержащим текущее время. Благодаря вызову setState(), React знает изменённое состояние, и вызывает метод render(), чтобы узнать, что должно быть на экране.  Это время, this.state.date в методе render() будет отличаться, и поэтому визуализирует вывод включая обновленное время. React обновит DOM-соответствующим образом.

5) Если компонент Clock когда-либо удалится из DOM, React вызовет хук componentWillUnmount(), который остановит таймер.

Корректное использование состояний

Есть три вещи, которые вы должны знать о setState().

Не меняйте состояние напрямую

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

// Wrong
this.state.comment = 'Hello';

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

// Правильно
this.setState({comment: 'Hello'});

Единственное место, где можно назначить this.state — конструктор.

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

 

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

Поскольку this.props и this.state могут обновляться асинхронно, нельзя полагаться на их значения для вычисления следующего состояния.

Например, этот код может не обновлять счетчик:

// Неправильно
this.setState({
  counter: this.state.counter + this.props.increment,
});

Для исправления, используйте вторую форму записи setState(), которая принимает функцию, а не объект. Эта функция будет получать предыдущее состояние в качестве первого аргумента и обновляемые свойства в качестве второго аргумента:

// Правильно
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

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

// Правильно
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});

Объединение обновлений состояний

Когда вызывается setState(), React объединяет предоставляемый вами объект в текущее состояние.

Например, ваше состояние может содержать несколько независимых переменных:

  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.

Нисходящие потоки данных (The Data Flows Down)

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

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

Компонент может передать свое состояние вниз в качестве свойств для его дочерних компонентов:

 

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

То же самое справедливо для пользовательских компонентов:

<FormattedDate date={this.state.date} />

Компонент FormattedDate будет получать date в его свойствах и не знать, пришел ли он из состояния для Clock,  или был набран вручную

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

Попробовать на CodePen.

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

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

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

 

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

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

Попробовать на CodePen.

Каждый Clock устанавливает свой собственный таймер и обновляется независимо от других компонентов.

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