После того как вы узнали о порядке использования готовых DOM-компонентов, пора приступить к изучению способов создания собственных компонентов.
API-интерфейс для создания нового компонента имеет следующий вид:
var MyComponent = React.createClass({
/* спецификации */
});
В качестве «спецификаций» используется объект JavaScript, имеющий один обязательный метод render() и несколько необязательных методов и свойств. Самый простой пример может выглядеть следующим образом:
var Component = React.createClass({
render: function() {
return React.DOM.span(null, "I'm so custom");
}
});
Как видите, единственное, что нужно сделать, — это реализовать метод render(). Он должен возвратить React-компонент, именно поэтому вы видите в фрагменте кода компонент span; просто текст возвратить нельзя.
Использование вашего компонента в приложении похоже на использование DOM-компонентов:
ReactDOM.render(
React.createElement(Component),
document.getElementById("app")
);
Результат отображения вашего пользовательского компонента показан на рис. 2.1.
Один из способов создания экземпляра вашего компонента — применение метода React.createElement(). При создании сразу нескольких экземпляров применяется фабрика:
var ComponentFactory = React.createFactory(Component);
ReactDOM.render(
ComponentFactory(),
document.getElementById("app")
);
Учтите, что уже известные вам методы семейства React.DOM.* фактически являются всего лишь удобными оболочками вокруг React.createElement(). Иными словами, этот код также работает с DOM-компонентами:
ReactDOM.render(
React.createElement("span", null, "Hello"),
document.getElementById("app")
);
Рис. 2.1. Ваш первый пользовательский компонент
Как видите, DOM-элементы, в отличие от функций JavaScript, определяются в виде строк, как и в случае с пользовательскими компонентами.
Ваши компоненты получают свойства и в зависимости от их значений по-разному выводятся на экран или ведут себя в приложении. Все свойства доступны через объект this.props. Рассмотрим пример:
var Component = React.createClass({
render: function() {
return React.DOM.span(null, "My name is " +
this.props.name);
}
});
Передача свойства при отображении компонента выглядит следующим образом:
ReactDOM.render(
React.createElement(Component, {
name: "Bob",
}),
document.getElementById("app")
);
Результат показан на рис. 2.2.
Свойство this.props следует считать пригодным только для чтения. Свойства можно с успехом применять для переноса настроек из родительских компонентов в дочерние (и, как будет показано далее, из дочерних компонентов — в родительские). Если вас все же что-то подстегивает назначить свойство с применением this.props, просто воспользуйтесь вместо этого дополнительными переменными или свойствами спецификации объекта вашего компонента (как в образце this.thing в противоположность образцу this.props.thing). На деле в браузерах, отвечающих спецификации ECMAScript5, вам не захочется видоизменять свойство this.props, поскольку:
> Object.isFrozen(this.props) === true; // истина
Рис. 2.2. Использование свойств компонента
Для объявления списка свойств, принимаемых вашим компонентом, и их типов вы можете добавить в свои компоненты свойство propTypes (типы свойств). Рассмотрим пример:
var Component = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
},
render: function() {
return React.DOM.span(null, "My name is " + this.props.name);
}
});
Свойство propTypes не обязательно использовать, но оно предоставляет два преимущества:
• вы заранее объявляете, какие свойства ожидает ваш компонент. Пользователям вашего компонента не придется тщательно изучать исходный (потенциально весьма длинный) код функции render(), чтобы определить, какими свойствами можно воспользоваться для настройки компонента;
• React проводит проверку значений свойств в ходе выполнения программы, поэтому свою функцию render() можно создавать без лишних опасений (или чрезмерной подозрительности) насчет данных, получаемых вашими компонентами.
Рассмотрим проверку в действии. Выражение name: React.PropTypes.string.isRequired явно просит для свойства name обязательное строковое значение. Если вы забудете передать значение, в консоли появится предупреждение (рис. 2.3):
ReactDOM.render(
React.createElement(Component, {
// name: "Bob",
}),
document.getElementById("app")
);
Предупреждение также будет получено при предоставлении значения неверного типа, скажем целого числа (рис. 2.4):
React.createElement(Component, {
name: 123,
})
Рис. 2.3. Предупреждение, появляющееся в случае, когда обязательное свойство не предоставлено
Рис. 2.4. Предупреждение, которое появляется при предоставлении значения неверного типа
Рисунок 2.5 даст представление обо всех доступных свойствах PropTypes, которыми можно воспользоваться для объявления ваших ожиданий.
Рис. 2.5. Список всех типов, используемых в React.PropTypes
Объявление propTypes в ваших компонентах не является обязательным (это означает, что здесь можно перечислить лишь некоторые, а не абсолютно все свойства). Можно назвать идею объявления не всех свойств порочной, но имейте в виду, что можете столкнуться с этим при отладке чужого кода.
Значения свойств, используемые по умолчанию. Когда ваши компоненты получают необязательные свойства, следует уделить особое внимание их работоспособности в том случае, когда свойства не объявляются. Это неизбежно приводит к применению защитного шаблона, например:
var text = 'text' in this.props ? this.props.text : '';
Избавиться от необходимости написания такого кода (сконцентрировавшись на более важных аспектах программы) можно, реализовав метод getDefaultProps():
var Component = React.createClass({
propTypes: {
firstName: React.PropTypes.string.isRequired,
middleName: React.PropTypes.string,
familyName: React.PropTypes.string.isRequired,
address: React.PropTypes.string,
},
getDefaultProps: function() {
return {
middleName: '',
address: 'n/a',
};
},
render: function() {/* ... */}
});
Как видите, getDefaultProps() возвращает объект, предоставляя допустимые значения для каждого необязательного свойства (из числа тех, для которых не указывается .isRequired).
До сих пор примеры были довольно статичными (или «не имеющими состояния»). Они преследовали простую цель: дать вам представление о создании блоков при составлении вашего пользовательского интерфейса. Но истинный блеск React (на фоне усложнения ситуации при работе с традиционной DOM-моделью браузера) проявляется, когда изменяются данные в вашем приложении. В React используется понятие состояния, которое представляет собой данные, используемые вашими компонентами для их самостоятельного отображения на экране. При изменении состояния React перестраивает пользовательский интерфейс без какого-либо вашего участия. Таким образом, после начального создания пользовательского интерфейса (в функции render()) вам останется лишь обновлять данные, совершенно не заботясь насчет изменений пользовательского интерфейса. По сути, ваш метод render() уже предоставил план внешнего вида компонента.
Обновление пользовательского интерфейса после вызова метода setState() выполняется с использованием механизма выстраивания очереди, который рационально группирует изменения, поэтому непосредственное обновление this.state (чего не следует делать) может привести к непредсказуемому поведению. Считайте, что объект this.state, так же как и this.props, предназначен лишь для чтения — не только потому, что его применение в другом качестве считается неприемлемым, но и потому, что такое применение может привести к неожиданным результатам. Аналогично никогда не вызывайте this.render() самостоятельно, лучше предоставьте такое право библиотеке React, чтобы дать ей возможность сгруппировать изменения, выявить их наименьшее количество и вызвать render() в самый подходящий момент.
Точно так же, как свойства доступны путем обращения к объекту this.props, к состоянию можно обратиться через объект this.state. Для обновления состояния предназначен метод this.setState(). При вызове this.setState() React вызывает ваш метод render() и обновляет пользовательский интерфейс.
React обновляет пользовательский интерфейс при вызове setState(). Это наиболее распространенный вариант, но чуть позже вы увидите, что есть еще и запасной вариант. Предотвратить обновление пользовательского интерфейса можно, вернув false в специальном методе жизненного цикла — shouldComponentUpdate().
Создадим новый компонент — textarea (текстовая область), сохраняющий количество набранных в нем символов (рис. 2.6).
Рис. 2.6. Конечный результат работы пользовательского компонента textarea
Вы (как и все другие потребители этого многократно используемого компонента) можете воспользоваться новым компонентом следующим образом:
ReactDOM.render(
React.createElement(TextAreaCounter, {
text: "Bob",
}),
document.getElementById("app")
);
Теперь реализуем компонент. Начнем с создания версии «без состояния», не обрабатывающей обновления, поскольку она не слишком отличается от всех предыдущих примеров:
var TextAreaCounter = React.createClass({
propTypes: {
text: React.PropTypes.string,
},
getDefaultProps: function() {
return {
text: '',
};
},
render: function() {
return React.DOM.div(null,
React.DOM.textarea({
defaultValue: this.props.text,
}),
React.DOM.h3(null, this.props.text.length)
);
}
});
Возможно, вы заметили, что textarea в предыдущем фрагменте кода получает свойство defaultValue, в отличие от привычного вам дочернего элемента text в HTML. Все дело в небольшой разнице между React и традиционным HTML, которая проявляется при работе с формами. Этот вопрос рассматривается в главе 4, и спешу заверить, что различий не так уж и много. Кроме того, вы поймете, что эти различия вполне резонны и призваны упростить вашу жизнь разработчика.
Как видите, компонент получает необязательное строковое свойство text и выводит текстовую область с заданным значением, а также получает элемент <h3>, который показывает длину строки (рис. 2.7).
Следующий шаг заключается в превращении этого лишенного состояния компонента в компонент с поддержкой состояния. Иными словами, получим компонент, обрабатывающий некие данные (состояние) и использующий их для исходного вывода самого себя на экран с последующим самостоятельным обновлением (повторным выводом) при их изменении.
Рис. 2.7. Компонент TextAreaCounter в работе
Реализуйте в вашем компоненте метод getInitialState(), чтобы быть уверенными, что работа всегда будет вестись с допустимыми данными:
getInitialState: function() {
return {
text: this.props.text,
};
},
Данные, обрабатываемые этим компонентом, являются простым текстом в текстовой области, поэтому у состояния есть только одно свойство text, доступ к которому можно получить через выражение this.state.text. Изначально (в getInitialState()) вы просто копируете свойство text. Позже при изменении данных (в ходе набора пользователем текста в текстовой области) компонент обновляет свое состояние, используя вспомогательный метод:
_textChange: function(ev) {
this.setState({
text: ev.target.value,
});
},
Состояние всегда обновляется с помощью метода this.setState(), который получает объект и объединяет его с уже существующими в this.state данными. Нетрудно догадаться, что _textChange() является отслеживателем событий, который получает объект события ev и извлекает из него текст, введенный в текстовую область.
Остается лишь обновить метод render() для использования вместо this.props свойства this.state и установить отслеживатель событий:
render: function() {
return React.DOM.div(null,
React.DOM.textarea({
value: this.state.text,
onChange: this._textChange,
}),
React.DOM.h3(null, this.state.text.length)
);
}
Теперь, как только пользователь что-нибудь набирает в текстовой области, значение счетчика обновляется, чтобы соответствовать содержимому этой области (рис. 2.8).
Рис. 2.8. Набор текста в текстовой области
Во избежание путаницы насчет следующей строки необходимо кое-что пояснить:
onChange: this._textChange
В целях повышения производительности, а также для удобства работы React использует собственную систему искусственно создаваемых событий. Чтобы легче было разобраться в причинах этого, следует рассмотреть, как все происходит в подлинном мире DOM-модели.
Чтобы выполнять какие-либо действия, очень удобно применять встроенные обработчики событий:
<button onclick="doStuff">
При всем удобстве и легкой узнаваемости (отслеживатель событий находится там же, где и пользовательский интерфейс) пользоваться слишком большим количеством разбросанных подобным образом отслеживателей событий крайне неэффективно. Также трудно пользоваться на одной и той же кнопке более чем одним отслеживателем, особенно если эта кнопка является не вашим, а чьим-то «компонентом» или входит в другую библиотеку и вам не хочется туда внедряться и «править» или разветвлять код. Именно поэтому в мире DOM-модели для установки отслеживателей используются метод element.addEventListener (что приводит к наличию кода в двух и более местах) и делегирование событий (для решения проблем производительности). Делегирование событий означает, что отслеживание событий осуществляется в родительском узле, скажем в <div>, содержащем множество кнопок, и для всех кнопок устанавливается один отслеживатель.
С использованием делегирования событий выполняется следующее:
<div id="parent">
<button id="ok">OK</button>
<button id="cancel">Cancel</button>
</div>
<script>
document.getElementById('parent').addEventListener('click',
function(event) {
var button = event.target;
// Выполнение разных действий на основе того,
// какая из кнопок была нажата
switch (button.id) {
case 'ok':
console.log('OK!');
break;
case 'cancel':
console.log('Cancel');
break;
default:
new Error('Непредвиденный идентификатор кнопки');
};
});
</script>
Со своей работой этот код справляется, но у него имеются недостатки:
• объявление отслеживателя находится далеко от компонента пользовательского интерфейса, что затрудняет поиск и отладку кода;
• делегирование с неизменным использованием инструкции switch создает ненужный шаблонный код непосредственно перед переходом к реальным действиям (в данном случае к реакции на нажатие кнопки);
• браузерная несовместимость (которая здесь не рассматривается) требует от этого кода более пространного решения.
К сожалению, как только дело доходит до практического применения данного кода реальными пользователями, для его поддержки всеми браузерами требуется предпринять ряд дополнительных мер:
• в дополнение к методу addEventListener требуется применение метода attachEvent;
• в самом начале кода отслеживателя требуется применение выражения var event = event || window.event;;
• требуется применение выражения var button = event.target || event.srcElement;.
Все эти необходимые и весьма неприятные нюансы в конечном итоге наводят на мысль о применении какой-нибудь библиотеки, связанной с обработкой событий. Но зачем добавлять еще одну библиотеку (и изучать дополнительные API-интерфейсы), когда React поставляется в комплекте с решениями, избавляющими от всех неприятностей, связанных с обработкой событий?
Чтобы охватить и привести к единому формату все браузерные события, в React используются искусственные события, ликвидирующие проблему несовместимости браузеров. Это позволяет вам быть уверенными, что свойство event.target доступно во всех браузерах. Именно поэтому в фрагменте кода TextAreaCounter нужно лишь воспользоваться свойством ev.target.value — и все заработает. Это также означает, что API-интерфейс для отмены событий един для всех браузеров; иными словами, методы event.stopPropagation() и event.preventDefault() работают даже на старых версиях IE.
Синтаксис упрощает совместную поддержку элементов пользовательского интерфейса и отслеживателей событий. Он похож на традиционные встроенные обработчики событий, но за кулисами все обстоит иначе. Фактически React в целях повышения производительности использует делегирование событий.
Для обработчиков событий в React применяется синтаксис с использованием «верблюжьего» регистра букв (camelCase), поэтому вместо onclick используется форма записи onClick.
Если по каким-то причинам вам понадобится исходное браузерное событие, оно доступно в виде свойства event.nativeEvent, но вряд ли вам это когда-либо пригодится.
И еще: событие onChange (в том же виде, в каком оно использовалось в примере с текстовой областью) ведет себя согласно вашим ожиданиям: оно инициируется, когда пользователь выполняет набор текста, а не после того, как набор будет завершен и произойдет переход за пределы данного поля, как это происходит в обычной DOM-модели.
Вы уже знаете, что при решении задачи отображения вашего компонента в методе render() у вас есть доступ к свойствам через выражение this.props и к состоянию — через this.state. Может возникнуть вопрос, когда следует использовать одно из этих выражений, а когда — другое.
Свойства — это механизм, предназначенный для внешнего мира (для пользователей компонента) и служащий для настройки вашего компонента. А состояние относится к работе с внутренними данными. Поэтому, если искать аналогии в объектно-ориентированном программировании, this.props является подобием аргументов, переданных конструктору класса, а this.state можно представить в виде набора ваших закрытых свойств.
Ранее уже был показан пример использования выражения this.props внутри метода getInitialState():
getInitialState: function() {
return {
text: this.props.text,
};
},
Фактически этот пример относится к антишаблонам. В идеале используется любая комбинация выражений this.state и this.props, подходящая для создания пользовательского интерфейса в методе render().
Однако иногда приходится брать значение, переданное вашему компоненту, и использовать его для построения исходного состояния. Здесь нет ничего крамольного, за исключением того, что код, вызывающий ваш компонент, может предполагать, что свойство (text в предыдущем примере) всегда будет иметь самое последнее значение, а пример противоречит этому предположению. Чтобы предположения не вызывали никаких сомнений, достаточно просто воспользоваться другим именем, например вместо text назвать свойство как-нибудь вроде defaultText или initialValue:
propTypes: {
defaultValue: React.PropTypes.string
},
getInitialState: function() {
return {
text: this.props.defaultValue,
};
},
В главе 4 показано, как в библиотеке React решается эта проблема для ее собственной реализации полей ввода и текстовых областей, где у кого-то могут возникать предположения, основанные на имеющемся опыте работы с HTML.
Позволить себе роскошь запускать совершенно новое React-приложение можно не всегда. Порой приходится внедряться в существующее приложение или в сайт и постепенно переходить к React. К счастью, библиотека React была сконструирована для работы с любым ранее созданным и имеющимся в вашем распоряжении базовым кодом. Ведь те, кто начинал создавать React, не могли переписать целиком огромное приложение (Facebook).
Один из способов, позволяющих вашему React-приложению общаться с внешним миром, заключается в получении ссылки на выводимый вами компонент с помощью метода ReactDOM.render() и ее использовании за пределами компонента:
var myTextAreaCounter = ReactDOM.render(
React.createElement(TextAreaCounter, {
defaultValue: "Bob",
}),
document.getElementById("app")
);
Теперь можно воспользоваться компонентом myTextAreaCounter для доступа к тем же методам и свойствам, к которым обычно обращаются с помощью выражения this внутри компонента. Можно даже манипулировать компонентами, используя вашу консоль JavaScript (рис. 2.9).
Рис. 2.9. Обращение к выведенному на экран компоненту с помощью ссылки
В этой строке кода устанавливается новое состояние:
myTextAreaCounter.setState({text: "Hello outside
world!"});
В этой строке кода берется ссылка на основной родительский DOM-узел, создаваемый React:
var reactAppNode = ReactDOM.findDOMNode(myTextAreaCounter);
Это первый дочерний элемент родительского <div id="app">, где библиотеке React предписывается творить чудеса:
reactAppNode.parentNode === document.getElementById('app'); // true
А вот как можно получить доступ к свойствам и состоянию:
myTextAreaCounter.props; // Object { defaultValue: "Bob"}
myTextAreaCounter.state;
// Object { text: "Hello outside world!"}
Вы получили доступ ко всему API-интерфейсу компонента за пределами вашего компонента. Но пользоваться этими сверхвозможностями следует весьма осмотрительно и только при крайней необходимости. Возможно, ReactDOM.findDOMNode() придется воспользоваться, если нужно получить габаритные размеры узла, чтобы убедиться, что он помещается на всю вашу страницу, но не более того. Может быть, манипулирование состоянием компонентов, владельцем которых вы не являетесь, и их подгонка покажутся вам весьма заманчивой затеей, но не стоит плодить ошибки, поскольку компонент не предполагает подобных вторжений. Например, следующий код вполне работоспособен, но использовать его не рекомендуется:
// Антипример
myTextAreaCounter.setState({text: 'NOOOO'});
Как известно, настройка компонента осуществляется с помощью его свойств.
Соответственно, изменение свойств извне после того, как компонент был создан, должно быть обоснованным. Но ваш компонент должен быть готов к обработке подобного развития событий.
Если посмотреть на метод render() из предыдущих примеров, в нем используется лишь выражение this.state:
render: function() {
return React.DOM.div(null,
React.DOM.textarea({
value: this.state.text,
nChange: this._textChange,
}),
React.DOM.h3(null, this.state.text.length)
);
}
Если свойства изменятся за пределами компонента, это не возымеет никакого эффекта на экране. Иными словами, содержимое текстовой области после того, как вы сделаете следующее, не изменится:
myTextAreaCounter = ReactDOM.render(
React.createElement(TextAreaCounter, {
defaultValue: "Hello", // ранее известное как "Bob"
}),
document.getElementById("app")
);
Даже притом что компонент myTextAreaCounter переписан с использованием нового вызова ReactDOM.render(), состояние приложения останется прежним. React ничего не стирает, а сопоставляет состояние приложения до и после. При этом он вносит минимальные изменения.
Теперь содержимое this.props изменилось (но содержимое пользовательского интерфейса осталось прежним):
myTextAreaCounter.props; // Object { defaultValue="Hello"}
Обновление пользовательского интерфейса выполняется путем установки состояния:
// Антипример
myTextAreaCounter.setState({text: 'Hello'});
Но это порочная практика, поскольку она может привести к несогласованному состоянию в более сложных компонентах, например внести путаницу во внутренние счетчики, булевы флаги, отслеживатели событий и т.д.
Если нужно корректно справиться с внешним вторжением (изменением свойств), к нему можно приготовиться, реализовав метод componentWillReceiveProps():
componentWillReceiveProps: function(newProps) {
this.setState({
text: newProps.defaultValue,
});
},
Как видите, этот метод получает новый объект свойств, и вы можете соответствующим образом установить состояние, а также выполнить любые другие действия, требующиеся для поддержания компонента в рабочем состоянии.
Метод componentWillReceiveProps() из предыдущего фрагмента кода относится к так называемым методам управления жизненным циклом, предлагаемым React. Эти методы можно использовать для отслеживания изменений в ваших компонентах. К числу других методов управления жизненным циклом, которые могут быть вами реализованы, относятся следующие.
• componentWillUpdate(). Выполняется до того, как метод render() вашего компонента будет вызван еще раз (в результате изменений свойств или состояния).
• componentDidUpdate(). Выполняется после завершения работы метода render() и применения новых изменений в отношении исходной DOM-модели.
• componentWillMount(). Выполняется перед вставкой узла в DOM-модель.
• componentDidMount(). Выполняется после вставки узла в DOM-модель.
• componentWillUnmount(). Выполняется непосредственно перед тем, как компонент удаляется из DOM-модели.
• shouldComponentUpdate(newProps, newState). Вызывается перед вызовом метода componentWillUpdate() и дает возможность возвратить false и отменить обновление, что означает отказ от вызова метода render(). Пригодится в тех местах приложения, для которых критична производительность, когда вы полагаете, что ничего важного не изменилось и повторный вывод на экран необязателен. Это решение принимается на основе сравнения аргумента newState с существующим значением выражения this.state и сравнения newProps с this.props или просто на основании сведений о том, что компонент статичен и не подвергается изменениям. (Соответствующий пример будет вскоре рассмотрен.)
Чтобы лучше разобраться в жизни компонента, добавим к компоненту TextAreaCounter регистрацию. Просто реализуем все методы управления жизненным циклом для регистрации их вызова наряду с показом всех их аргументов в консоли:
var TextAreaCounter = React.createClass({
_log: function(methodName, args) {
console.log(methodName, args);
},
componentWillUpdate: function() {
this._log('componentWillUpdate', arguments);
},
componentDidUpdate: function() {
this._log('componentDidUpdate', arguments);
},
componentWillMount: function() {
this._log('componentWillMount', arguments);
},
componentDidMount: function() {
this._log('componentDidMount', arguments);
},
componentWillUnmount: function() {
this._log('componentWillUnmount', arguments);
},
// ...
// продолжение реализации, render() и т.д.
};
Все происходящее после загрузки страницы показано на рис. 2.10.
Рис. 2.10. Установка компонента
Как видите, были вызваны два метода без аргументов. Обычно из этих двух методов наиболее интересен componentDidMount(). Если нужно, доступ к только что установленному DOM-узлу можно получить путем вызова метода ReactDOM.findDOMNode(this) (например, для получения габаритных размеров компонента). Теперь, когда ваш компонент ожил, можно выполнять с ним любые действия по инициализации.
Интересно, что произойдет, если набрать s, превращая текст в Bobs? (См. рис. 2.11.)
Рис. 2.11. Обновление компонента
Будет вызван метод componentWillUpdate(nextProps, nextState) с новыми данными, которые будут использованы для повторного отображения компонента. Первый аргумент представляет собой будущее значение свойства this.props (которое в данном примере не изменяется), а второй — будущее значение нового свойства this.state. Третьим по значимости является контекст context, который на данном этапе особого интереса для нас не представляет. Вы можете сравнить аргументы (например, newProps) с текущим значением this.props и решить, нужно ли совершать с ними какие-либо действия.
Как видите, после метода componentWillUpdate() вызван метод componentDidUpdate(oldProps, oldState) с передачей ему значений свойств и состояния до изменения. Он позволяет работать с DOM, когда компонент уже обновлен. В нем можно указать выражение this.setState(), чего нельзя сделать в componentWillUpdate().
Предположим, что вам требуется ограничить количество символов, набираемых в текстовой области. Это нужно делать в обработчике события _textChange(), вызываемом при наборе пользователем текста. Но что, если кто-нибудь (кто наивнее и моложе вас) вызовет setState() за пределами компонента? (Как уже ранее упоминалось, такой вызов — порочная практика.) Сможете ли вы защитить согласованность и нормальное состояние своего компонента? Разумеется. Вы можете провести проверку в методе componentDidUpdate() и, если количество символов превысит разрешенное, вернуть состояние к его прежнему значению. То есть нужно сделать нечто подобное:
componentDidUpdate: function(oldProps, oldState) {
if (this.state.text.length > 3) {
this.replaceState(oldState);
}
},
Хотя подобные опасения и излишни, в случае чего всегда можно отыграть все назад.
Обратите внимание на использование метода replaceState() вместо setState(). Метод setState(obj) объединяет свойства obj с теми, которые имелись в this.state, а метод replaceState() полностью все перезаписывает.
В предыдущем примере были показаны регистрируемые вызовы четырех из пяти методов управления жизненным циклом. Пятый метод, componentWillUnmount(), лучше всего продемонстрировать при наличии дочерних компонентов, удаляемых их родителем. В этом примере вам нужно зарегистрировать все изменения в обоих дочерних компонентах и в родительском компоненте. Поэтому введем новое понятие для многократно используемого кода: примесь, или миксин (mixin).
Примесью называют объект JavaScript, содержащий коллекцию методов и свойств. Примеси предназначены не для самостоятельного использования, а для включения (подмешивания) в свойства другого объекта. В примере регистрации примесь может иметь следующий вид:
var logMixin = {
_log: function(methodName, args) {
console.log(this.name + '::' + methodName, args);
},
componentWillUpdate: function() {
this._log('componentWillUpdate', arguments);
},
componentDidUpdate: function() {
this._log('componentDidUpdate', arguments);
},
componentWillMount: function() {
this._log('componentWillMount', arguments);
},
componentDidMount: function() {
this._log('componentDidMount', arguments);
},
componentWillUnmount: function() {
this._log('componentWillUnmount', arguments);
},
};
В мире, где React не используется, можно применить цикл for-in и скопировать все свойства в новый объект, получив все функциональные возможности примеси. В мире React в вашем распоряжении имеется сокращенная форма записи: свойство mixins, имеющее следующий вид:
var MyComponent = React.createClass({
mixins: [obj1, obj2, obj3],
// все остальные методы ...
};
Вы присваиваете свойству mixins массив объектов JavaScript, а React берет на себя всю остальную работу. Включение logMixin в ваш компонент выглядит следующим образом:
var TextAreaCounter = React.createClass({
name: 'TextAreaCounter',
mixins: [logMixin],
// все остальное ..
};
Как видите, во фрагменте кода также добавляется удобное свойство name, чтобы идентифицировать вызывающий код.
Если запустить пример с примесью, вы увидите регистрацию в действии (рис. 2.12).
Рис. 2.12. Использование примеси и идентификация компонента
Как известно, React-компоненты могут смешиваться и вкладываться друг в друга любым нужным вам образом. До сих пор в методах render() вы видели только компоненты React.DOM (и не видели пользовательских компонентов). Посмотрим на простой пользовательский компонент, применяемый в качестве дочернего.
Часть счетчика может быть выделена в свой собственный компонент:
var Counter = React.createClass({
name: 'Counter',
mixins: [logMixin],
propTypes: {
count: React.PropTypes.number.isRequired,
},
render: function() {
return React.DOM.span(null, this.props.count);
}
});
Этот компонент составляет только часть счетчика — он выводит на экран контейнер <span> и не работает с состоянием, а только отображает свойство count, задаваемое родительским компонентом. Он также подмешивает logMixin для регистрации вызовов методов управления жизненным циклом.
Теперь обновим метод render() родительского компонента TextAreaCounter. При определенных условиях он должен использовать компонент Counter, и если count имеет значение 0 — число даже не показывается:
render: function() {
var counter = null;
if (this.state.text.length > 0) {
counter = React.DOM.h3(null,
React.createElement(Counter, {
count: this.state.text.length,
})
);
}
return React.DOM.div(null,
React.DOM.textarea({
value: this.state.text,
onChange: this._textChange,
}),
counter
);
}
Когда текстовая область пуста, переменная counter имеет значение null. Когда в ней имеется какой-нибудь текст, переменная counter содержит часть пользовательского интерфейса, отвечающую за отображение количества символов. Быть встроенным в качестве аргументов главного компонента React.DOM.div всему пользовательскому интерфейсу нет никакой необходимости. Вы можете присвоить фрагменты пользовательского интерфейса переменным и применять их при возникновении определенных условий.
Теперь можно наблюдать за регистрируемыми методами управления жизненным циклом для обоих компонентов. На рис. 2.13 показано, что произойдет, когда загрузится страница, а затем изменится содержимое текстовой области.
Рис. 2.13. Установка и обновление двух компонентов
Можно отследить, как дочерний компонент устанавливается и обновляется раньше своего родительского компонента.
На рис. 2.14 показано, что произойдет после удаления текста из текстовой области; значение count станет нулевым. В этом случае дочерний компонент Counter получит значение null и его DOM-узел будет удален из дерева DOM-модели после того, как вас уведомят с помощью функции обратного вызова componentWillUnmount.
Рис. 2.14. Демонтаж компонента counter
Последний метод управления жизненным циклом, о котором вы должны знать, особенно при создании критичных к производительности частей вашего приложения, называется shouldComponentUpdate(nextProps, nextState). Он вызывается перед вызовом метода componentWillUpdate() и предоставляет вам возможность отменить обновление (если вы посчитаете его ненужным).
Существует класс компонентов, использующих в своих методах render() только this.props и this.state, не прибегая к дополнительным вызовам функций. Эти компоненты называются чистыми компонентами. Они могут реализовать метод shouldComponentUpdate() и сравнить состояние и свойства до и после, а при отсутствии каких-либо изменений — возвращать false, экономя при этом долю вычислительных мощностей. Кроме того, бывают чисто статичные компоненты, не использующие ни свойства, ни состояние. Такие компоненты могут сразу же возвращать false.
Посмотрим, что произойдет с вызовом методов render() и реализацией shouldComponentUpdate() для получения выгоды в отношении производительности.
Сначала возьмем новый компонент Counter. Удалим из него примесь регистрирования и вместо этого станем отправлять регистрационную запись на консоль при каждом вызове метода render():
var Counter = React.createClass({
name: 'Counter',
// mixins: [logMixin],
propTypes: {
count: React.PropTypes.number.isRequired,
},
render() {
console.log(this.name + '::render()');
return React.DOM.span(null, this.props.count);
}
});
Сделаем то же самое в компоненте TextAreaCounter:
var TextAreaCounter = React.createClass({
name: 'TextAreaCounter',
// mixins: [logMixin],
// все остальные методы ...
render: function() {
console.log(this.name + '::render()');
// ... и вся остальная часть вывода на экран
}
});
Когда страница будет загружена и вместо "Bob" будет вставлена строка "LOL", вы сможете увидеть результат, показанный на рис. 2.15.
Рис. 2.15. Повторный вывод на экран обоих компонентов
Как видите, обновление текста приводит к вызову метода render() компонента TextAreaCounter, который, в свою очередь, становится причиной вызова метода render() компонента Counter. При замене "Bob" строкой "LOL" количество символов до и после обновления не меняется, следовательно, изменений в пользовательском интерфейсе счетчика не происходит и в вызове метода render() компонента Counter нет никакой необходимости. Вы можете помочь React оптимизировать этот случай, реализовав метод shouldComponentUpdate() и возвратив false, когда в последующем выводе на экран нет необходимости. Метод получает будущие значения свойств и состояния (в состоянии данный компонент не нуждается) и внутри себя сравнивает текущие и следующие значения:
shouldComponentUpdate(nextProps, nextState_ignore) {
return nextProps.count !== this.props.count;
},
Выполнение замены "Bob" на "LOL" не заставляет больше Counter заново выводиться на экран (рис. 2.16).
Рис. 2.16. Выигрыш в производительности: экономия одного цикла повторного вывода на экран
Реализация shouldComponentUpdate() не отличается особой сложностью. И не такая уж трудная задача — превратить эту реализацию в универсальную, поскольку вы всегда сравниваете this.props с nextProps и this.state с nextState. React предоставляет одну такую универсальную реализацию в виде примеси, которую можно включать в любой компонент.
Вот как это делается:
<script src="react/build/react-with-addons.js"></script>
<script src="react/build/react-dom.js"></script>
<script>
var Counter = React.createClass({
name: 'Counter',
mixins: [React.addons.PureRenderMixin],
propTypes: {
count: React.PropTypes.number.isRequired,
},
render: function() {
console.log(this.name + '::render()');
return React.DOM.span(null, this.props.count);
}
});
// ...
</script>
Результат (рис. 2.17) не отличается от предыдущего — когда количество символов не меняется, метод render() компонента Counter не вызывается.
Следует отметить, что примесь PureRenderMixin не является частью ядра React, но входит в расширенную версию дополнений к React. Следовательно, чтобы получить к нему доступ, вместо react/build/react.js следует включить в код react/build/react-with-addons.js. Это предоставит вам новое пространство имен React.addons, где наряду с другими полезными дополнениями можно найти и PureRenderMixin.
Рис. 2.17. Упрощенное получение выигрыша в производительности: подмешивание PureRenderMixin
Если не хочется включать все дополнения или нужно реализовать собственную версию примеси, не стесняйтесь заглянуть в реализацию. Она предельно проста и понятна и служит всего лишь оболочкой для проверки равенства, представляя собой нечто подобное:
var ReactComponentWithPureRenderMixin = {
shouldComponentUpdate: function(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
};