Состояние и жизненный цикл¶
На этой странице представлены понятия «состояние» (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-приложениях, имеет ли компонент состояние или нет — это внутренняя деталь реализации компонента, которая может меняться со временем. Можно использовать компоненты без состояния в компонентах с состоянием, и наоборот.