Перевод статьи «5 Practical Examples For Learning The React Framework», Martin Angelov
Вы вероятно слышали о популярном JavaScript фреймворке от Facebook – React. Он используется на многих популярных веб-сайтах, в том числе в Facebook и Instagram. В этой статье вы увидите 5 практических примеров, построенных при помощи React, которые помогут вам начать работать с этим фреймворком.
Что особого в фреймворке React?
React построен на концепции компонентов. Он отличается от таких фреймворков, как Angular или Ember, которые используют двухстороннюю привязку данных для обновления HTML страницы. На мой взгляд, React проще для изучения, чем Angular или Ember – он намного меньше и хорошо работает с jQuery и другими фреймворками. Он, к тому же, чрезвычайно быстр, так как использует виртуальный DOM и обновляет только измененные части страницы (обращение к DOM до сих пор является, самой медленной частью современных веб-приложений, поэтому данный фреймворк и получает преимущество в производительности оптимизируя его).
Однако, обратная сторона монеты это то, что в React нужно немного больше кода для достижения вещей, которые могут быть запросто написаны при помощи привязки данных, в чем вы можете убедиться в примерах ниже. Например посмотрите нашу заметку об Angular'е.
Как его использовать?
Чтобы использовать React, вам нужно включить подключить единственный JavaScript файл. Facebook любезно предоставляет нам CDN, на который вы можете напрямую сослаться:
<script src="http://fb.me/react-0.10.0.min.js"></script>
Это даст вам доступ к глобальному объекту React, который содержит множество полезных методов, некоторые из них вы можете видеть в наших примерах. Рекомендованный способ написания веб-приложений на React, это используя язык JSX. Это слегка измененная версия JavaScript, которая позволит вам писать React компоненты используя HTML-подобный синтаксис прямо в вашем коде. Этот код компилируется в JavaScript, перед тем как быть интерпретированным браузером. JSX совсем необязателен – вы можете использовать обычный JavaScript, если предпочитаете.
Но хватит трепаться, давайте увидим немного кода!
1. Таймер
Как я упоминал раньше, строительные блоки приложений react, это компоненты. Они создаются при помощи вызова React.createClass()
передавая объект со свойствами и методами. Каждый компонент имеет состояние (объект с данными) и является ответственным за свое отображение – метод render()
вызывается, когда изменяется состояние. Вот пример построения обычного таймера:
/** @jsx React.DOM */
// Создаем компонент вызовом React.createClass.
var TimerExample = React.createClass({
getInitialState: function(){
// Это выполняется перед функцией render. Возвращаемый объект
// присваивается в this.state, чтобы мы могли использовать его позже.
return { elapsed: 0 };
},
componentDidMount: function(){
// componentDidMount вызывается react'ом, когда компонент
// был отрисован на странице. Мы можем установить интервал здесь:
this.timer = setInterval(this.tick, 50);
},
componentWillUnmount: function(){
// Этот метод вызывается сразу после того, как компонент удален
// со страницы и уничтожен. Мы можем удалить интервал здесь:
clearInterval(this.timer);
},
tick: function(){
// Эта функция вызывается каждые 50мс. Она обновляет
// счетчик затраченного времени. Вызов setState заставляет компонент перерисовываться
this.setState({elapsed: new Date() - this.props.start});
},
render: function() {
var elapsed = Math.round(this.state.elapsed / 100);
// Это даст нам число с одной цифрой после запятой dot (xx.x):
var seconds = (elapsed / 10).toFixed(1);
// Хоть мы и возвращаем целый <p> элемент, react разумно обновит
// только измененные части, содержащие переменную seconds.
return <p>This example was started <b>{seconds} seconds</b> ago.</p>;
}
});
React.renderComponent(
<TimerExample start={Date.now()} />,
document.body
);
Вы можете заключить любое JavaScript выражение между скобок {}, когда создаете компонент. В этом примере мы поставляем начальную дату, которая будет использована при каждом вызове функции tick()
для расчета затраченных секунд.
2. Меню навигации
Давайте посмотрим, как мы можем обрабатывать события клика в React, построив меню навигации:
/** @jsx React.DOM */
var MenuExample = React.createClass({
getInitialState: function(){
return { focused: 0 };
},
clicked: function(index){
// Обработчик клика обновит состояние
// изменив индекс на сфокусированный элемент меню
this.setState({focused: index});
},
render: function() {
// Здесь мы считаем свойство items, которое было передано
// атрибутом, при создании компонента
var self = this;
// Метод map пройдется по массиву элементов меню,
// и возвратит массив с <li> элементами.
return (
<div>
<ul>{ this.props.items.map(function(m, index){
var style = '';
if(self.state.focused == index){
style = 'focused';
}
// Обратите внимание на использование метода bind(). Он делает
// index доступным в функции clicked:
return <li className={style} onClick={self.clicked.bind(self, index)}>{m}</li>;
}) }
</ul>
<p>Selected: {this.props.items[this.state.focused]}</p>
</div>
);
}
});
// Отображаем компонент меню на странице, передав ему массив с элементами
React.renderComponent(
<MenuExample items={ ['Home', 'Services', 'About', 'Contact us'] } />,
document.body
);
Вы, вероятно, заметили в коде данного примера, что мы используем атрибуты, как например className, которых, на самом деле не существует в HTML. Это из-за того, что когда мы возвращаем элемент , мы на самом деле не возвращаем HTML, а только объект компонента React.DOM.p
. Вы можете узнать об этом больше по этой ссылке.
3. Мгновенный поиск
Мы узнали, снова и снова, что люди ненавидят ждать. Вот как вы можете использовать React для построения мгновенного поиска. (Для сравнения, смотрите нашу версию с AngularJS).
/** @jsx React.DOM */
// Давайте создадим компонент мгновенного поиска
var SearchExample = React.createClass({
getInitialState: function(){
return { searchString: '' };
},
handleChange: function(e){
// Если вы закомментируете данную строку, поле ввода не изменит свое значение.
// Это потому, что в React'е, поле не может измениться независимо от свойства
// которое было ему присвоено. В нашем случае, это this.state.searchString.
this.setState({searchString:e.target.value});
},
render: function() {
var libraries = this.props.items,
searchString = this.state.searchString.trim().toLowerCase();
if(searchString.length > 0){
// Ищем. Фильтрируем резальтаты.
libraries = libraries.filter(function(l){
return l.name.toLowerCase().match( searchString );
});
}
return <div>
<input type="text" value={this.state.searchString} onChange={this.handleChange} placeholder="Type here" />
<ul>
{ libraries.map(function(l){
return <li>{l.name} <a href={l.url}>{l.url}</a></li>
}) }
</ul>
</div>;
}
});
var libraries = [
{ name: 'Backbone.js', url: 'http://documentcloud.github.io/backbone/'},
{ name: 'AngularJS', url: 'https://angularjs.org/'},
{ name: 'jQuery', url: 'http://jquery.com/'},
{ name: 'Prototype', url: 'http://www.prototypejs.org/'},
{ name: 'React', url: 'http://facebook.github.io/react/'},
{ name: 'Ember', url: 'http://emberjs.com/'},
{ name: 'Knockout.js', url: 'http://knockoutjs.com/'},
{ name: 'Dojo', url: 'http://dojotoolkit.org/'},
{ name: 'Mootools', url: 'http://mootools.net/'},
{ name: 'Underscore', url: 'http://documentcloud.github.io/underscore/'},
{ name: 'Lodash', url: 'http://lodash.com/'},
{ name: 'Moment', url: 'http://momentjs.com/'},
{ name: 'Express', url: 'http://expressjs.com/'},
{ name: 'Koa', url: 'http://koajs.com/'},
];
// Отображаем компонент SearchExample на странице
React.renderComponent(
<SearchExample items={ libraries } />,
document.body
);
Этот пример также демонстрирует нам ещё одну важную концепцию в React – обновление состояния нашего контроллера влияет на HTML, а не наоборот. В этом примере, как только в установили значение текстового поля, оно будет оставаться прежним, не учитывая каждое нажатие клавиш, до тех пор, пока вы не измените его событием onChange (вы можете изменить это поведение, но это рекомендованный способ делать подобные вещи).
4. Форма заказа
Настоящая мощь React`а проявляется, когда вы сочетаете несколько компонентов. Это позволяет вам лучше структурировать ваш код и ввести разделение обязанностей. Это также делает ваш код более легко используемым между разными частями вашего приложения. Вот пример формы заказа, которая позволяет клиентам планировать их бюджет (смотрите Angular версию):
// Это более сложный пример, который использует два компоненты -
// форма выбора сервиса и индивидуальные сервисы внутри.
var ServiceChooser = React.createClass({
getInitialState: function(){
return { total: 0 };
},
addTotal: function( price ){
this.setState( { total: this.state.total + price } );
},
render: function() {
var self = this;
var services = this.props.items.map(function(s){
// Создадим новый экземпляр компонента Service для каждого элемента массива.
// Заметьте, что мы передаем функцию self.addTotal function в компонент.
return <Service name={s.name} price={s.price} active={s.active} addTotal={self.addTotal} />;
});
return <div>
<h1>Our services</h1>
<div id="services">
{services}
<p id="total">Total <b>${this.state.total.toFixed(2)}</b></p>
</div>
</div>;
}
});
var Service = React.createClass({
getInitialState: function(){
return { active: false };
},
clickHandler: function (){
var active = !this.state.active;
this.setState({ active: active });
// сообщаем ServiceChooser, вызывая метод addTotal
this.props.addTotal( active ? this.props.price : -this.props.price );
},
render: function(){
return <p className={ this.state.active ? 'active' : '' } onClick={this.clickHandler}>
{this.props.name} <b>${this.props.price.toFixed(2)}</b>
</p>;
}
});
var services = [
{ name: 'Web Development', price: 300 },
{ name: 'Design', price: 400 },
{ name: 'Integration', price: 250 },
{ name: 'Training', price: 220 }
];
// Отображаем ServiceChooser и передаем ему массив сервисов
React.renderComponent(
<ServiceChooser items={ services } />,
document.body
);
Первая проблема возникающая при сочетании нескольких компонентов, это как позволить им общаться между собой. Первый способ, это передавать необходимые данные атрибутами при их инициализации. Это работает только в случае общения компонентов «от родительского к дочернему». Для общения в другом направлении, вы можете передать один из методов родительского компонента атрибутом в дочерний компонент. Дочерний компонент может потом вызывать этот метод и сообщать родителю о событиях. Вы можете прочитать об этом здесь.
5. Приложение с изображениями на AJAX
Этот пример покажет, как вы можете комбинировать react с jQuery, и как загружать результаты при через AJAX. В то время как, фреймворки, как Angular имеют свои собственные подходы для работы с AJAX, React позволяет вам использовать любые библиотеки, с которыми вам удобнее работать (смотрите Angular версию).
/** @jsx React.DOM */
// В этом примере мы также имеем два компонента - изображение и список изображений
// Изображения получены из Instagram через AJAX.
var Picture = React.createClass({
// Этот компонент не содержит никакого состояния - он просто преобразует
// то что было передано атрибутами в изображение.
clickHandler: function(){
// Когда компонент кликнут, вызываем обработчик onClick,
// который был передан атрибутом при создании:
this.props.onClick(this.props.ref);
},
render: function(){
var cls = 'picture ' + (this.props.favorite ? 'favorite' : '');
return (
<div className={cls} onClick={this.clickHandler}>
<img src={this.props.src} width="200" title={this.props.title} />
</div>
);
}
});
var PictureList = React.createClass({
getInitialState: function(){
// Массив изображений будет передан по AJAX, а
// избранные когда, пользователь кликнет по изображению:
return { pictures: [], favorites: [] };
},
componentDidMount: function(){
// Когда компонент загружается, отправляем jQuery AJAX запрос
var self = this;
// конечная точка API, для загрузки популярных изображений дня
var url = 'https://api.instagram.com/v1/media/popular?client_id=' + this.props.apiKey + '&callback=?';
$.getJSON(url, function(result){
if(!result || !result.data || !result.data.length){
return;
}
var pictures = result.data.map(function(p){
return {
id: p.id,
url: p.link,
src: p.images.low_resolution.url,
title: p.caption ? p.caption.text : '',
favorite: false
};
});
// Обновляем состояние компонента. Это вызовет render.
// Заметьте, что это обновляет только свойство pictures
// и не удаляет массив избранных.
self.setState({ pictures: pictures });
});
},
pictureClick: function(id){
// id содержит ID кликнутого изображения.
// Найдем в массиве pictures и добавим его в избранные
var favorites = this.state.favorites,
pictures = this.state.pictures;
for(var i = 0; i < pictures.length; i++){
// Находим айди в массиве изображений
if(pictures[i].id == id) {
if(pictures[i].favorite){
return this.favoriteClick(id);
}
// Добавляем изображение в массив избранных,
// и отмечаем, как избранное:
favorites.push(pictures[i]);
pictures[i].favorite = true;
break;
}
}
// Обновляем состояние, вызывая перерисовку
this.setState({pictures: pictures, favorites: favorites});
},
favoriteClick: function(id){
// Находим изображение в списке избранных и удалаяем его
// После этого находим изображение в массиве всех изображений и отмечаем, как не-избранное.
var favorites = this.state.favorites,
pictures = this.state.pictures;
for(var i = 0; i < favorites.length; i++){
if(favorites[i].id == id) break;
}
// Удаляем из избранных
favorites.splice(i, 1);
for(i = 0; i < pictures.length; i++){
if(pictures[i].id == id) {
pictures[i].favorite = false;
break;
}
}
// Обновляем состояние и перерисовываем
this.setState({pictures: pictures, favorites: favorites});
},
render: function() {
var self = this;
var pictures = this.state.pictures.map(function(p){
return <Picture ref={p.id} src={p.src} title={p.title} favorite={p.favorite} onClick={self.pictureClick} />
});
if(!pictures.length){
pictures = <p>Loading images..</p>;
}
var favorites = this.state.favorites.map(function(p){
return <Picture ref={p.id} src={p.src} title={p.title} favorite={true} onClick={self.favoriteClick} />
});
if(!favorites.length){
favorites = <p>Click an image to mark it as a favorite.</p>;
}
return (
<div>
<h1>Popular Instagram pics</h1>
<div className="pictures"> {pictures} </div>
<h1>Your favorites</h1>
<div className="favorites"> {favorites} </div>
</div>
);
}
});
// Отрисовываем компонент PictureList и добавлем его в body.
// я использую API ключ для тестового Instagram приложения.
// Пожалуйста, сгенерируйте и используйте свой собственный здесь http://instagram.com/developer/
React.renderComponent(
<PictureList apiKey="642176ece1e7445e99244cec26f4de1f" />,
document.body
);
полный пример JSFiddle
Заметьте, что в наш примере используется один и тот же компонент Picture для отображения списка всех и избранных изображений. Такое повторное использование является одним из преимуществ для организации вашего кода в компоненты.
Что дальше?
React позволяет вам прозрачно структурировать ваше приложение и способствует повторному использованию кода. И благодаря мощному виртуальному dom, фреймворк может значительно ускорять сложные интерфейсы. React подходит для рендеринга на серверной стороне так же хорошо, как и на клиентской и делает возможным построение изоморфных приложений, легко переходящих с клиента на сервер.
Но есть еще много чего, что нужно изучить о react. Я предлагаю начать отсюда:
- Главная страница React
- Руководство для начинающих
- Детальный пошаговый урок
- React дополнения
- Тег на stackoverflow
Автор: jojo97
насколько я понимаю использование var self = this; в функции Рендер является лишним, подойдет и простая передача методов через this.*, такие функции будут запущены в нужном контексте родительского компонента.