Состояние и жизненный цикл¶
На этой странице представлены понятия «состояние» (state) и «жизненный цикл» (lifecycle) React-компонентов.
В качестве примера рассмотрим идущие часы из предыдущего раздела. В главе Рендеринг элементов мы научились обновлять UI только одним способом — вызовом ReactDOM.render()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В этой главе мы узнаем, как инкапсулировать и обеспечить многократное использование компонента Clock
. Компонент самостоятельно установит свой собственный таймер и будет обновляться раз в секунду.
Для начала, извлечём компонент, показывающий время:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Проблема в том, что компонент Clock
не обновляет себя каждую секунду автоматически. Хотелось бы спрятать логику, управляющую таймером, внутри самого компонента Clock
.
В идеале мы бы хотели реализовать Clock
таким образом, чтобы компонент сам себя обновлял:
1 |
|
Для этого добавим так называемое «состояние» (state) в компонент Clock
.
«Состояние» очень похоже на уже знакомые нам пропсы, отличие в том, что состояние управляется и доступно только конкретному компоненту.
Преобразование функционального компонента в классовый¶
Давайте преобразуем функциональный компонент Clock
в классовый компонент за 5 шагов:
-
Создаём ES6-класс с таким же именем, указываем
React.Component
в качестве родительского класса -
Добавим в класс пустой метод
render()
-
Перенесём тело функции в метод
render()
-
Заменим
props
наthis.props
в телеrender()
-
Удалим оставшееся пустое объявление функции
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Теперь Clock
определён как класс, а не функция.
Метод render
будет вызываться каждый раз, когда происходит обновление. Так как мы рендерим <Clock />
в один и тот же DOM-контейнер, мы используем единственный экземпляр класса Clock
— поэтому мы можем задействовать внутреннее состояние и методы жизненного цикла.
Добавим внутреннее состояние в класс¶
Переместим date
из пропсов в состояние в три этапа:
1. Заменим this.props.date
на this.state.date
в методе render()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
2. Добавим конструктор класса, в котором укажем начальное состояние в переменной this.state
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Обратите внимание, что мы передаём props
базовому (родительскому) конструктору:
1 2 3 4 |
|
Классовые компоненты всегда должны вызывать базовый конструктор с аргументом props
.
3. Удалим проп date
из элемента <Clock />
:
1 |
|
Позже мы вернём код таймера обратно и на этот раз поместим его в сам компонент.
Результат выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Теперь осталось только установить собственный таймер внутри Clock
и обновлять компонент каждую секунду.
Добавим методы жизненного цикла в класс¶
В приложениях с множеством компонентов очень важно освобождать используемые системные ресурсы когда компоненты удаляются.
Первоначальный рендеринг компонента в DOM называется «монтирование» (mounting). Нам нужно устанавливать таймер всякий раз, когда это происходит.
Каждый раз когда DOM-узел, созданный компонентом, удаляется, происходит «размонтирование» (unmounting). Чтобы избежать утечки ресурсов, мы будем сбрасывать таймер при каждом «размонтировании».
Объявим специальные методы, которые компонент будет вызывать при монтировании и размонтировании:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Эти методы называются «методами жизненного цикла» (lifecycle methods).
Метод componentDidMount()
запускается после того, как компонент отрендерился в DOM — здесь мы и установим таймер:
1 2 3 4 5 6 |
|
Обратите внимание, что мы сохраняем ID таймера в this
.
Поля this.props
и this.state
в классах особенные, и их устанавливает сам React. Вы можете вручную добавить новые поля, если компоненту нужно хранить дополнительную информацию (например, ID таймера).
Теперь нам осталось сбросить таймер в методе жизненного цикла componentWillUnmount()
:
1 2 3 |
|
Наконец, реализуем метод tick()
. Он запускается таймером каждую секунду и вызывает this.setState()
.
this.setState()
планирует обновление внутреннего состояния компонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Теперь часы обновляются каждую секунду.
Давайте рассмотрим наше решение и разберём порядок, в котором вызываются методы:
-
Когда мы передаём
<Clock />
вReactDOM.render()
, React вызывает конструктор компонента.Clock
должен отображать текущее время, поэтому мы задаём начальное состояниеthis.state
объектом с текущим временем. -
React вызывает метод
render()
компонентаClock
. Таким образом React узнаёт, что отобразить на экране. Далее, React обновляет DOM так, чтобы он соответствовал выводу рендераClock
. -
Как только вывод рендера
Clock
вставлен в DOM, React вызывает метод жизненного циклаcomponentDidMount()
. Внутри него компонентClock
указывает браузеру установить таймер, который будет вызыватьtick()
раз в секунду. -
Таймер вызывает
tick()
ежесекундно. Внутриtick()
мы просим React обновить состояние компонента, вызываяsetState()
с текущим временем. React реагирует на изменение состояния и снова запускаетrender()
. На этот разthis.state.date
в методеrender()
содержит новое значение, поэтому React заменит DOM. Таким образом компонентClock
каждую секунду обновляет UI. -
Если компонент
Clock
когда-либо удалится из DOM, React вызовет метод жизненного циклаcomponentWillUnmount()
и сбросит таймер.
Как правильно использовать состояние¶
Важно знать три детали о правильном применении setState()
.
Не изменяйте состояние напрямую¶
В следующем примере повторного рендера не происходит:
1 2 |
|
Вместо этого используйте setState()
:
1 2 |
|
Конструктор — это единственное место, где вы можете присвоить значение this.state
напрямую.
Обновления состояния могут быть асинхронными¶
React может сгруппировать несколько вызовов setState()
в одно обновление для улучшения производительности.
Поскольку this.props
и this.state
могут обновляться асинхронно, вы не должны полагаться на их текущее значение для вычисления следующего состояния.
Например, следующий код может не обновить счётчик:
1 2 3 4 |
|
Правильно будет использовать второй вариант вызова setState()
, который принимает функцию, а не объект. Эта функция получит предыдущее состояние в качестве первого аргумента и значения пропсов непосредственно во время обновления в качестве второго аргумента:
1 2 3 4 |
|
В данном примере мы использовали стрелочную функцию, но можно использовать и обычные функции:
1 2 3 4 5 6 |
|
Обновления состояния объединяются¶
Когда мы вызываем setState()
, React объединит аргумент (новое состояние) c текущим состоянием.
Например, состояние может состоять из нескольких независимых полей:
1 2 3 4 5 6 7 |
|
Их можно обновлять по отдельности с помощью отдельных вызовов setState()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Состояния объединяются поверхностно, поэтому вызов this.setState({comments})
оставляет this.state.posts
нетронутым, но полностью заменяет this.state.comments
.
Однонаправленный поток данных¶
В иерархии компонентов, ни родительский, ни дочерние компоненты не знают, задано ли состояние другого компонента. Также не важно, как был создан определённый компонент — с помощью функции или класса.
Состояние часто называют «локальным», «внутренним» или инкапсулированным. Оно доступно только для самого компонента и скрыто от других.
Компонент может передать своё состояние вниз по дереву в виде пропсов дочерних компонентов:
1 |
|
Своё состояние можно передать и другому пользовательскому компоненту:
1 |
|
Компонент FormattedDate
получает date
через пропсы, но он не знает, откуда они взялись изначально — из состояния Clock
, пропсов Clock
или просто JavaScript-выражения:
1 2 3 4 5 |
|
Этот процесс называется «нисходящим» ("top-down") или «однонаправленным» ("unidirectional") потоком данных. Состояние всегда принадлежит определённому компоненту, а любые производные этого состояния могут влиять только на компоненты, находящиеся «ниже» в дереве компонентов.
Если представить иерархию компонентов как водопад пропсов, то состояние каждого компонента похоже на дополнительный источник, который сливается с водопадом в произвольной точке, но также течёт вниз.
Чтобы показать, что все компоненты действительно изолированы, создадим компонент App
, который рендерит три компонента <Clock>
:
1 2 3 4 5 6 7 8 9 10 11 |
|
У каждого компонента Clock
есть собственное состояние таймера, которое обновляется независимо от других компонентов.
В React-приложениях, имеет ли компонент состояние или нет — это внутренняя деталь реализации компонента, которая может меняться со временем. Можно использовать компоненты без состояния в компонентах с состоянием, и наоборот.