До сих пор свойства использовались только для обработки данных в компонентах React. Свойства имеют неизменяемый характер. После отображения свойства компонента не изменяются. Чтобы изменить пользовательский интерфейс, понадобится другой механизм, способный заново отобразить дерево компонента с новыми свойствами. Состояние React является его неотъемлемой частью, предназначенной для управления данными, которые будут изменяться внутри компонента. Когда состояние приложения меняется, пользовательский интерфейс отображается заново, чтобы отразить эти нововведения.
Пользователи взаимодействуют с приложениями. Они перемещаются по данным, ищут их, фильтруют, выбирают, добавляют, обновляют и удаляют. Когда пользователь работает с приложением, состояние программы изменяется, и эти перемены отображаются для пользователя в UI. Появляются и исчезают экраны и панели меню. Меняется видимое содержимое. Включаются и выключаются индикаторы. В React пользовательский интерфейс отражает состояние приложения.
Состояние может быть выражено в компонентах React с единственным объектом JavaScript. В момент изменения своего состояния компонент отображает новый пользовательский интерфейс, показывающий эти изменения. Что может быть функциональнее? Получая данные, компонент React отображает их в виде UI. На основе их изменения React будет обновлять интерфейс, чтобы отразить эти перемены наиболее рациональным образом.
Посмотрим, как можно встроить состояние в наши компоненты React.
Внедрение состояния компонента
Состояние представляет данные, которые при желании можно изменить внутри компонента. Чтобы показать это, рассмотрим компонент StarRating (рис. 6.7).
Этот компонент требует два важных фрагмента данных: общее количество звезд для отображения и рейтинг, или количество выделенных звезд.
Нам нужен компонент Star, реагирующий на щелчки кнопкой мыши и имеющий свойство selected. Для каждой звезды может использоваться функциональный компонент, не имеющий состояния:
const Star = ({ selected=false, onClick=f=>f }) =>
<div className={(selected) ? "star selected" : "star"}
onClick={onClick}>
</div>
Star.propTypes = {
selected: PropTypes.bool,
onClick: PropTypes.func
}
Каждый элемент звезды Star будет состоять из контейнера div, включающего атрибут class со значением 'star'. Если звезда выбрана, то к ней дополнительно будет добавлен атрибут class со значением 'selected'. У этого компонента также имеется дополнительное свойство onClick. Когда пользователь щелкает кнопкой мыши на контейнере div какой-нибудь звезды, вызывается данное свойство. Притом родительский компонент, StarRating, будет оповещен о щелчке на компоненте Star.
Компонент Star является функциональным, не имеющим состояния. Исходя из самого названия его категории, в таком компоненте использовать состояние невозможно. Их предназначение — входить в качестве дочерних в состав более сложных компонентов, имеющих состояние. Чем больше компонентов не имеет состояния, тем лучше.
Теперь, получив компонент Star, мы можем воспользоваться им для создания компонента StarRating. Из своих свойств компонент StarRating станет получать общее количество отображаемых звезд. А рейтинг, значение которого пользователь сможет изменять, будет сохранен в состоянии.
Сначала рассмотрим способ внедрения состояния в компонент, определенный с помощью метода createClass:
const StarRating = createClass({
displayName: 'StarRating',
propTypes: {
totalStars: PropTypes.number
},
getDefaultProps() {
return {
totalStars: 5
}
},
getInitialState() {
return {
starsSelected: 0
}
},
change(starsSelected) {
this.setState({starsSelected})
},
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i}
selected={i<starsSelected}
onClick={() => this.change(i+1)}
/>
)}
<p>{starsSelected} of {totalStars} stars</p>
</div>
)
}
})
Используя метод createClass, состояние можно инициализировать, добавив к конфигурации компонента метод getInitialState и возвратив объект JavaScript, который изначально устанавливает для переменной состояния starsSelected значение 0.
При отображении компонента общее количество звезд, totalStars, берется из свойств компонента и применяется с целью отобразить указанное количество элементов Star. При этом для инициализации нового массива указанной длины, отображаемого на элементы Star, с конструктором Array используется оператор распространения.
Когда компонент отображается на экране, переменная состояния starsSelected деструктурируется из элемента this.state. Он используется для отображения рейтинга в виде текста в элементе абзаца, а также для подсчета количества выбранных звезд, выводимых на экран. Каждый элемент Star получает свое свойство selected путем сравнения своего индекса с количеством выбранных звезд. Если выбрано три звезды, то первые три элемента Star установят для своего свойства selected значение true, а все остальные звезды будут иметь для него значение false.
И наконец, когда пользователь щелкает кнопкой мыши на отдельно взятой звезде, индекс конкретно этого элемента Star увеличивается на единицу и отправляется функции change. Данное значение увеличивается на единицу, поскольку предполагается наличие у первой звезды рейтинга 1, даже притом, что индекс у нее равен нулю.
Инициализация состояния в классе компонента ES6 немного отличается от аналогичного процесса с использованием метода createClass. В этих классах состояние может быть инициализировано в конструкторе:
class StarRating extends Component {
constructor(props) {
super(props)
this.state = {
starsSelected: 0
}
this.change = this.change.bind(this)
}
change(starsSelected) {
this.setState({starsSelected})
}
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i}
selected={i<starsSelected}
onClick={() => this.change(i+1)}
/>
)}
<p>{starsSelected} of {totalStars} stars</p>
</div>
)
}
}
StarRating.propTypes = {
totalStars: PropTypes.number
}
StarRating.defaultProps = {
totalStars: 5
}
При установке компонента ES6 вызывается его конструктор со свойствами, внедренными в качестве первого аргумента. В свою очередь, эти свойства отправляются родительскому классу путем вызова метода super. В данном случае родительским является класс React.Component. Вызов super инициализирует экземпляр компонента, а React.Component придает этому экземпляру функциональную отделку, включающую управление состоянием. После вызова super можно инициализировать переменные состояния нашего компонента.
После инициализации состояние функционирует точно так же, как и в компонентах, созданных с помощью метода createClass. Оно может быть изменено только путем вызова метода this.setState, который обновляет указанные части объекта состояния. После каждого вызова setState вызывается функция отображения render, обновляющая пользовательский интерфейс в соответствии с новым состоянием.
Инициализация состояния из свойств
Значения состояния можно инициализировать с помощью поступающих свойств. Применение этой схемы может быть вызвано всего лишь несколькими обстоятельствами. Чаще всего ее задействуют при создании компонента, предназначенного для многократного применения среди приложений в различных деревьях компонентов.
При использовании метода createClass весьма приемлемым способом инициализации переменных состояния на основе поступающих свойств будет добавление метода componentWillMount. Он вызывается один раз при установке компонента, и из этого метода можно вызвать метод this.setState(). В нем также имеется доступ к this.props; следовательно, чтобы помочь процессу инициализации состояния, можно воспользоваться значениями из this.props:
const StarRating = createClass({
displayName: 'StarRating',
propTypes: {
totalStars: PropTypes.number
},
getDefaultProps() {
return {
totalStars: 5
}
},
getInitialState() {
return {
starsSelected: 0
}
},
componentWillMount() {
const { starsSelected } = this.props
if (starsSelected) {
this.setState({starsSelected})
}
},
change(starsSelected) {
this.setState({starsSelected})
},
render() {
const {totalStars} = this.props
const {starsSelected} = this.state
return (
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i}
selected={i<starsSelected}
onClick={() => this.change(i+1)}
/>
)}
<p>{starsSelected} of {totalStars} stars</p>
</div>
)
}
})
render(
<StarRating totalStars={7} starsSelected={3} />,
document.getElementById('react-container')
)
Метод componentWillMount является частью жизненного цикла компонента. Он может использоваться для инициализации состояния на основе значений свойств в компонентах, созданных с помощью метода createClass, или в компонентах класса ES6. Более подробно жизненный цикл компонента будет рассмотрен в следующей главе.
Инициализировать состояние внутри класса компонента ES6 можно более простым способом. Конструктор получает свойства в виде аргумента, поэтому просто воспользуемся аргументом props, переданным конструктору:
constructor(props) {
super(props)
this.state = {
starsSelected: props.starsSelected || 0
}
this.change = this.change.bind(this)
}
В большинстве случаев нужно избегать установки переменных состояния из свойств. Подобными схемами стоит пользоваться только в самых крайних случаях. Добиться этого не трудно, поскольку при работе с компонентами React ваша задача заключается в ограничении количества компонентов, имеющих состояние.
Состояние внутри дерева компонента
Собственное состояние может быть у всех ваших компонентов, но должно ли оно у них быть? Причина удовольствия, получаемого от использования React, не кроется в охоте за переменными состояния по всему вашему приложению, а исходит из возможности создания простых в понимании масштабируемых приложений. Самое важное, что можно сделать с целью облегчить понимание вашей программы, — это свести к необходимому минимуму количество компонентов, использующих состояние.
Во многих приложениях React есть возможность сгруппировать все данные состояний в корневом компоненте. Данные состояний можно передать вниз по дереву компонентов через свойства, а вверх по дереву к корневому компоненту — через двустороннюю привязку функций. В результате все состояние вашего приложения в целом будет находиться в одном месте. Часто это называют «единым источником истины».
Далее мы рассмотрим, как создаются уровни презентации, где все состояния хранятся в одном месте, в корневом компоненте.
Новый взгляд на приложение органайзера цветов
Органайзер образцов цвета позволяет пользователям добавлять, называть, оценивать и удалять цвета в видоизменяемых ими списках. Все состояние органайзера может быть представлено с помощью одного массива:
{
colors: [
{
"id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
"title": "ocean at dusk",
"color": "#00c4e2",
"rating": 5
},
{
"id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
"title": "lawn",
"color": "#26ac56",
"rating": 3
},
{
"id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
"title": "bright red",
"color": "#ff0000",
"rating": 0
}
]
}
Из этого массива следует, что нам нужно отобразить три цвета: океана в сумерки (ocean at dusk), зеленого газона (lawn) и ярко-красного оттенка (bright red) (рис. 6.8). Массив предоставляет шестнадцатеричные значения, соответствующие тому или иному цвету, и текущий рейтинг для каждого из них, выводимый на экран. Он также дает возможность уникальной идентификации каждого цвета.
Эти данные состояния будут управлять приложением. Они станут использоваться для создания пользовательского интерфейса при каждом изменении этого объекта. Когда пользователи добавляют или удаляют цвета, эти образцы добавляются в массив или удаляются из него. Когда пользователи дают оценку цветам, рейтинги последних изменяются в массиве.
Передача свойств вниз по дереву компонентов
Ранее в данной главе был создан компонент StarRating, сохраняющий рейтинг в состоянии. В органайзере цветов рейтинг сохраняется в каждом объекте цвета. Намного рациональнее будет рассматривать StarRating в качестве презентационного компонента и объявлять его как функциональный компонент, не имеющий состояния. Презентационные компоненты отвечают только за образ, создаваемый приложением на экране. Они лишь отображают элементы DOM или другие презентационные компоненты. Все данные передаются этим компонентам через свойства, а из них передаются через функции обратного вызова.
Чтобы превратить компонент StarRating в чисто презентационный, нужно убрать из него состояние. В презентационных компонентах используются только свойства. Поскольку состояние из этого компонента убирается в момент, когда пользователь изменяет оценку, соответствующие данные будут переданы из данного компонента через функцию обратного вызова:
const StarRating = ({starsSelected=0, totalStars=5, onRate=f=>f}) =>
<div className="star-rating">
{[...Array(totalStars)].map((n, i) =>
<Star key={i}
selected={i<starsSelected}
onClick={() => onRate(i+1)}/>
)}
<p>{starsSelected} of {totalStars} stars</p>
</div>
Во-первых, starsSelected больше не является переменной состояния, теперь это свойство. Во-вторых, к данному компоненту добавлено свойство в onRate, представляющее собой функцию обратного вызова. Вместо того чтобы при изменении пользователем оценки вызвать setState, теперь starsSelected вызывает onRate и отправляет оценку в качестве аргумента.
Ограничение, накладываемое на состояние, — размещение его только в одном месте, в корневом компоненте, — означает, что все данные должны передаваться вниз дочерним компонентам в качестве свойств (рис. 6.9).
В органайзере состояние складывается из массива образцов цвета, объявленного в компоненте App. Эти цвета передаются вниз компоненту ColorList в качестве свойства:
class App extends Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
}
render() {
const { colors } = this.state
return (
<div className="app">
<AddColorForm />
<ColorList colors={colors} />
</div>
)
}
}
Изначально массив цветов пуст, поэтому компонент ColorList вместо каждого цвета будет отображать сообщение. Когда в массиве имеются цвета, данные для каждого отдельно взятого образца передаются компоненту Color в качестве свойств:
const ColorList = ({ colors=[] }) =>
<div className="color-list">
{(colors.length === 0) ?
<p>No Colors Listed. (Add a Color)</p> :
colors.map(color =>
<Color key={color.id} {...color} />
)
}
</div>
Теперь компонент Color может отобразить название и шестнадцатеричное значение цвета и передать его оценку вниз компоненту StarRating в качестве свойства:
const Color = ({ title, color, rating=0 }) =>
<section className="color">
<h1>{title}</h1>
<div className="color"
style={{ backgroundColor: color }}>
</div>
<div>
<StarRating starsSelected={rating} />
</div>
</section>
Количество выбранных звезд, starsSelected, в звездном рейтинге поступает из оценки каждого цвета. Все данные состояния для каждого цвета переданы вниз по дереву дочерним компонентам в качестве свойств. Когда в корневом компоненте происходят изменения в данных, React, чтобы отразить новое состояние, вносит изменения в пользовательский интерфейс наиболее рациональным образом.
Передача данных вверх по дереву компонентов
Состояние в органайзере цветов может быть обновлено только путем вызова метода setState из компонента App. Если пользователи инициируют какие-либо изменения из пользовательского интерфейса, то для обновления состояния введенные ими данные нужно будет передать вверх по дереву компонентов компоненту App (рис. 6.10). Эта задача может быть выполнена с помощью свойств в виде функций обратного вызова.
Чтобы добавить новые цвета, нужен способ уникальной идентификации каждого образца. Этот идентификатор будет использоваться для определения местоположения цвета в массиве состояния. Для создания абсолютно уникальных идентификаторов можно воспользоваться библиотекой uuid:
npm install uuid --save
Все новые цвета будут добавляться в органайзер из компонента AddColorForm, который был создан ранее в разделе «Ссылки». У этого компонента имеется дополнительное свойство в виде функции обратного вызова onNewColor. Когда пользователь добавляет новый цвет и отправляет данные формы, вызывается функция обратного вызова onNewColor с новым названием и шестнадцатеричным значением, полученными от пользователя:
import { Component } from 'react'
import { v4 } from 'uuid'
import AddColorForm from './AddColorForm'
import ColorList from './ColorList'
export class App extends Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
this.addColor = this.addColor.bind(this)
}
addColor(title, color) {
const colors = [
...this.state.colors,
{
id: v4(),
title,
color,
rating: 0
}
]
this.setState({colors})
}
render() {
const { addColor } = this
const { colors } = this.state
return (
<div className="app">
<AddColorForm onNewColor={addColor} />
<ColorList colors={colors} />
</div>
)
}
}
Все новые цвета могут быть добавлены из метода addColor в компонент App. Эта функция привязана к компоненту в конструкторе, следовательно, у нее есть доступ к this.state и к this.setState.
Новые цвета добавляются путем объединения текущего массива цветов с новым объектом цвета. Идентификатор для последнего устанавливается функцией v4, принадлежащей библиотеке uuid. Таким образом создается уникальный идентификатор для каждого цвета. Название и цвета передаются методу addColor из компонента AddColorForm. И наконец, исходным значением для оценки каждого цвета будет ноль.
Когда пользователь добавляет цвет с помощью компонента AddColorForm, метод addColor обновляет состояние, используя новый список цветов. Как только состояние будет обновлено, компонент App заново отобразит дерево компонентов, применяя новый список. После каждого вызова установки состояния setState вызывается метод отображения render. Новые данные передаются вниз по дереву в качестве свойств и служат для построения пользовательского интерфейса.
Если пользователь хочет оценить или удалить цвет, то нам нужно собрать информацию об этом образце. У каждого цвета будет кнопка удаления: если пользователь нажмет ее, то мы будем знать, что он хочет удалить данный цвет. Кроме того, в случае изменения пользователем оценки цвета с помощью компонента StarRating нам нужно изменить рейтинг этого цвета:
const Color = ({title,color,rating=0,onRemove=f=>f,onRate=f=>f}) =>
<section className="color">
<h1>{title}</h1>
<button onClick={onRemove}>X</button>
<div className="color"
style={{ backgroundColor: color }}>
</div>
<div>
<StarRating starsSelected={rating} onRate={onRate} />
</div>
</section>
Информация, которая будет изменяться в данном приложении, сохраняется в списке цветов. Поэтому к каждому цвету следует добавить свойства в виде функций обратного вызова onRemove и onRate, чтобы данные соответствующих событий удаления и оценки передавались вверх по дереву. Свойства в виде функций обратного вызова onRemove и onRate также будут иметься у компонента Color. Когда цвета оцениваются или удаляются, компонент ColorList должен оповестить свой родительский компонент App о том, что этот цвет нужно оценить или удалить:
const ColorList = ({ colors=[], onRate=f=>f, onRemove=f=>f }) =>
<div className="color-list">
{(colors.length === 0) ?
<p>No Colors Listed. (Add a Color)</p> :
colors.map(color =>
<Color key={color.id}
{...color}
onRate={(rating) => onRate(color.id, rating)}
onRemove={() => onRemove(color.id)} />
)
}
</div>
Компонент ColorList вызовет onRate, если какие-либо цвета оценены, или же onRemove при удалении каких-либо цветов. Этот компонент управляет коллекцией цветов, отображая их на отдельно взятые компоненты Color. Когда отдельно взятые цвета оцениваются или удаляются, ColorList идентифицирует цвет, который подвергся воздействию, и передает эту информацию своему родительскому компоненту через свойство в виде функции обратного вызова.
Родительским для ColorList является компонент App. В нем методы rateColor и removeColor могут быть добавлены и привязаны к экземпляру компонента в конструкторе. Как только понадобится оценить или удалить цвет, эти методы обновят состояние. Они добавлены к компоненту ColorList в качестве свойств в виде функций обратного вызова:
class App extends Component {
constructor(props) {
super(props)
this.state = {
colors: []
}
this.addColor = this.addColor.bind(this)
this.rateColor = this.rateColor.bind(this)
this.removeColor = this.removeColor.bind(this)
}
addColor(title, color) {
const colors = [
...this.state.colors,
{
id: v4(),
title,
color,
rating: 0
}
]
this.setState({colors})
}
rateColor(id, rating) {
const colors = this.state.colors.map(color =>
(color.id !== id) ?
color :
{
...color,
rating
}
)
this.setState({colors})
}
removeColor(id) {
const colors = this.state.colors.filter(
color => color.id !== id
)
this.setState({colors})
}
render() {
const { addColor, rateColor, removeColor } = this
const { colors } = this.state
return (
<div className="app">
<AddColorForm onNewColor={addColor} />
<ColorList colors={colors}
onRate={rateColor}
onRemove={removeColor} />
</div>
)
}
}
Оба метода: и rateColor, и removeColor — ожидают получения идентификатора того цвета, который оценивается или удаляется. ID записывается в компоненте ColorList и передается в качестве аргумента методам rateColor или removeColor. Метод rateColor находит оцениваемый цвет и изменяет его рейтинг в состоянии. Для создания нового массива состояния без удаленного цвета метод removeColor использует метод Array.filter.
После вызова метода setState пользовательский интерфейс отображается заново с новыми данными состояния. Все данные, изменившиеся в этом приложении, управляются из одного компонента, App. Приведенный подход существенно упрощает понимание того, какие именно данные используются приложением для создания состояния и как они будут изменены.
Компоненты React отличаются высокой надежностью. Они предоставляют четко выраженный способ управления свойствами и их проверки, обмена данными с дочерними элементами и управления данными состояния из-за пределов компонента. Эти свойства дают возможность создать превосходно масштабируемые уровни презентации.
Уже не раз упоминалось, что состояние предназначено для данных, подвергаемых изменениям. Состояние также можно использовать в вашем приложении для кэширования данных. Например, если имеется список записей, в котором пользователь может вести поиск, то этот список можно в ходе поиска сохранить в состоянии.
Часто высказываются рекомендации ограничивать присутствие состояния корневыми компонентами. Такой подход будет встречаться во многих приложениях React. Как только ваше приложение достигнет определенного размера, двусторонняя привязка данных и свойства, передаваемые явным образом, могут принести массу неудобств.
Чтобы управлять состоянием и сократить объем шаблонного кода в подобных ситуациях, можно применять шаблон проектирования Flux и Flux-библиотеки, подобные Redux.
React — относительно небольшая библиотека, и на данный момент мы рассмотрели основную часть ее функциональных возможностей. В следующей главе мы обсудим такие основные свойства компонентов React, как жизненный цикл компонента и компоненты высшего порядка.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 20% по купону — React
Комментарии (10)
dpischalka
16.01.2018 16:07Я уважаю O'Reilly, их книги помогают мне становится лучше, и эта книга думаю помогла бы начать работать с React. Правильно подобрана концепция работы с данными, т.е. Redux. Который сейчас «на коне». Хотелось бы чтобы автор освятил такие темы как ImmutableJS и работу с ним в React.
И ещё чего я понять не могу. Почему мы до сих пор как-то обосабливаем ES6? Уже все к нему привыкли и давно на нём пишут, но всё равно в материалах часто встречаешь понятия «новый синтаксис». Не пора ли уже рассматривать ES2015 как часть языка?
Издательству спасибо за книгу.alexeymorozovv
17.01.2018 09:05Я думаю, ES2015 можно будет рассматривать как «часть языка» после того, как во всех браузерах (хотя бы тех, которыми действительно пользуются) он будет поддерживаться без транспайлеров.
dpischalka
17.01.2018 13:49Это тоже уже не объективно. Не только браузер исполняет JS код. А транспайлеры будут всегда, ведь мы хотим использовать все прелести нововведений не дожидаясь полной поддержки интерпретаторами, верно?
alexeymorozovv
17.01.2018 14:11Нововведения обычно вводятся (сори за тавтологию) новыми спецификация, и ES2015 одна из таких спецификаций, если она не поддерживается интерпретаторами, то как мы можем говорить, что это часть языка? Это как если бы в какой-нибудь C#, в существующую спецификацию (версию), добавили конструкцию, но уточнили, что компилироваться это не будет. С Node, по мне с этим проще, мы просто говорим, вот новая версия, там полностью поддерживается ES2015, обновитесь и профит.
Apathetic
19.01.2018 00:21Какие именно браузеры вам нужны? Актуальными версиями всех мажорных браузеров ES6 поддерживается на 96-99%.
http://kangax.github.io/compat-table/es6/
MrCheater
Одному из первых довелось прочитать эту книгу. Хорошее руководство для начинающего frontend-разработчика. Здесь обо всём понемногу. Для быстрого старта это самое то.
Свежий React+Redux на практике; Введение в функциональное программирование; Новый синтаксис ES6, JSX; Общие сведения про тестирование и линтинг (Jest + ESLint); Новый React-Router (v4); И даже раскрыта тема изоморфных приложений и Server Side Rendering.