React
Alexey Volkov
in practice
alexey@rumble.me
- Since May 2013
- Current version 0.13.1
- Popular and it's growing
https://github.com/facebook/
react/wiki/Sites-Using-React
Everything is a component
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
var Timer = React.createClass({
getInitialState: function() {
return {secondsElapsed: 0};
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div>Seconds Elapsed: {this.state.secondsElapsed}</div>
);
}
});
React.render(<Timer />, mountNode);
- Just the UI ("V" in MVC)
- Virtual DOM
- Synthetic events
- Isomorphic
- One-way data flow
- NOT A FRAMEWORK
Advantages
- Declarative
- Performance
- Isomorphic
- Nice learning curve
- Nice approach for separation of concerns
- Not a framework
- React way = JavaScript way
- Something else?
Disadvantages
- Not a silver bullet
- Not a framework
- Scattering
- JSX
- Young
- Something else?
JSX hurts? Let's split it out
var Timer = React.createClass({
getInitialState: function() {
return {secondsElapsed: 0};
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: require('./Timer.render.js')
});
React.render(React.createElement(Timer), mountNode);
"When I first looked at React I thought it was insane like most people. It takes such a different approach to web development that it gives many people an immediate repulsive reaction. But of course the more I used it the more I realised I could never go back to building web applications (or any front-end app for that matter) any other way. The patterns react provides are an extremely powerful way of building applications.
-
Backbone Models
-
CSS
-
Mobile Web
-
D3
Backbone Models? Why?
- Simple
- Well-documented
- Large community
- A long list of modules/plugins
React + Backbone
mixins: [mixinForm({
viewModelName: 'PushCampaign'
})],
...
return <div>
<c.Header title='< Push Campaign Editor' level={2} innerRight={buttons} />
<c.Fieldset caption='Message Composer'>
<c.FieldRow caption='Campaign Name:'
className={this.form().labelClassName('Name')}
hint={this.form().errors('Name')}>
<c.Input type='text' {...this.form().attribute('Name')} />
</c.FieldRow>
<c.FieldRow caption='Description:' subCaption='For internal use only'>
<c.Input type='textarea' {...this.form().attribute('Notes')} />
</c.FieldRow>
</c.Fieldset>
<c.Fieldset caption='Message Targeting'>
<c.FieldRow caption='Send Notification To:'
className={this.form().labelClassName('_targeting')}
hint={this.form().errors('_targeting')}>
<c.RadioList items={targetingItems} {...this.form().attribute('_targeting')} />
</c.FieldRow>
</c.Fieldset>
<c.Header level={2} innerRight={buttons} />
</div>;
return <div>
<c.Header title='< Push Campaign Editor' level={2} innerRight={buttons} />
<c.Fieldset caption='Message Composer'>
<c.FieldRow caption='Campaign Name:'
className={this.form().labelClassName('Name')}
hint={this.form().errors('Name')}>
<c.Input type='text' value={this.form().value('Name')}
className={this.form().inputClassName('Name')}
onChange={this.form().handleChange('Name')}
/>
</c.FieldRow>
<c.FieldRow caption='Description:' subCaption='For internal use only'>
<c.Input type='textarea' {...this.form().attribute('Notes')} />
</c.FieldRow>
</c.Fieldset>
<c.Fieldset caption='Message Targeting'>
<c.FieldRow caption='Send Notification To:'
className={this.form().labelClassName('_targeting')}
hint={this.form().errors('_targeting')}>
<c.RadioList items={targetingItems} {...this.form().attribute('_targeting')} />
</c.FieldRow>
</c.Fieldset>
<c.Header level={2} innerRight={buttons} />
</div>;
componentWillMount: function () {
var model = this.form().model;
if (model.get('RegistrationId')) {
model.set('_targeting', 'id');
}
// hide error message for RegistrationId (if user switched targeting type to "All")
model.on('change:_targeting', function (model) {
if (model.get('_targeting') === 'all') {
model.set('RegistrationId', '');
this.form().validate({RegistrationId: model.get('RegistrationId')});
}
}, this);
// clear link validation status on link editing
model.on('change:ArticleUrl', function (model) {
model.set('_articleUrlValid', null);
if (_.isEmpty(model.get('ArticleUrl'))) {
this.setState({notificationLink: LINK_EMPTY});
} else {
this.forceUpdate();
}
}, this);
},
componentWillUnmount: function () {
this.form().model.off(null, null, this);
},
var PushCampaign = Backbone.Model.extend({
defaults: {
'Name': '',
'Message': '',
'_targeting': 'all'
},
validation: {
Name: {
required: true
},
Message: {
required: true,
maxLength: 250
},
RegistrationId: function (value) {
if (this.get('_targeting') === 'id' && _.isEmpty(value)) {
return 'Registration ID is missed';
}
}
}
});
<FillPicker {...this.form().attribute('brandColors[name=main]')} />
<ImageUploader
value={this.form().value('icons[name=ipad152x152].uri')}
onChange={this.form().onChange('icons[name=ipad152x152].uri')}
hint="152x152" deletable={true}
/>
module.exports = Backbone.AssociatedModel.extend({
...
relations: [
{
key: 'icons',
type: Backbone.Many,
relatedModel: 'Image',
collectionType: 'ImagesCollection'
},
{
key: 'brandColors',
type: Backbone.Many,
relatedModel: Backbone.AssociatedModel.getRelatedModel('Fill'),
collectionType: 'FillsCollection'
}
]
...
});
https://github.com/RumbleInc/react-validator-mixin
npm install
react-validator-mixin
React + Backbone
- Form data validation
- Models in flux stores
- Router
- <Your ideas>?
CSS. What is the problem?
- One global namespace
- Dependencies
- Sharing constants
CSS. Solutions?
- "It's not a bug. It's a feature!"
- LESS/SASS/...
- BEM (block-element-modificator)
https://github.com/albburtsev/bem-cn - inline styles
- -anything: else?
- React-Styler
'use strict';
var React = require('react/lib/ReactWithAddons'),
styler = require('react-styler');
var Fieldset = React.createClass({
mixins: [styler.mixinFor('Fieldset')],
render: function () {
var cn = this.className;
/* jshint ignore:start */
return <fieldset className={cn()} style={this.props.style}>
{this.props.caption &&
<legend className={cn('caption')}>{this.props.caption}</legend>}
{this.props.children}
</fieldset>;
/* jshint ignore:end */
}
});
module.exports = Fieldset;
React.render(<Fieldset />, mountNode);
<fieldset class="Fieldset">
<legend class="Fieldset-caption">Caption</legend>
...
</fieldset>
React.render(<Fieldset className="group1" />, mountNode);
<fieldset class="Fieldset Fieldset-group1">
<legend class="Fieldset-caption">Caption</legend>
...
</fieldset>
styler.registerComponentStyles('Fieldset', {
border: '1px solid #dbdbdb',
padding: '35px 20px 20px',
'& + &': {
marginTop: 25
},
'&-caption': {
color: '#474747',
padding: '0 8px',
marginLeft: -8
}
});
styler.registerComponentStyles('ChartPushNotifications', {
'& .LineChart-lines path:nth-of-type(2)': {
transform: 'translateY(-2px)'
},
'& .LineChart-background': {
fill: '#f5f5f5'
},
'&-tooltip': {
padding: 10,
textAlign: 'center'
},
'&-tooltip-channel': {
fontSize: 12,
maxWidth: 180
},
'&-tooltip-channel:before': {
content: '"\\2588"',
display: 'inline-block',
width: 10,
height: 10,
overflow: 'hidden',
verticalAlign: 'middle',
marginRight: 5,
fontSize: '1em',
lineHeight: '10px'
}
});
.ChartPushNotifications {
}
.ChartPushNotifications .LineChart-lines path:nth-of-type(2) {
transform: translateY(-2px);
}
.ChartPushNotifications .LineChart-background {
fill: #f5f5f5;
}
.ChartPushNotifications-tooltip {
padding: 10px;
text-align: center;
}
.ChartPushNotifications-tooltip-channel {
font-size: 12px;
max-width: 180px;
}
.ChartPushNotifications-tooltip-channel:before {
content: "\2588";
display: inline-block;
width: 10px;
height: 10px;
overflow: hidden;
vertical-align: middle;
margin-right: 5px;
font-size: 1em;
line-height: 10px;
}
Styler. Advantages?
- BEM-like syntax
- Dependencies
- Namespaces
- Constants/variables
- Very easy to learn and use
https://github.com/RumbleInc/react-styler
npm install react-styler
React + Mobile Web
React + Mobile Web
- Minimize DOM modifications
(use React, avoid jQuery) - Optimise Virtual DOM
(use shouldComponentUpdate) - Be smart with CSS
React + Mobile Web
- react-canvas (from Flipboard)
- react-native (congrats! just went public)
Not WEB, it's native!
React + D3
React + D3. Approaches
- React as a wrapper
- React as a renderer
render: function() {
return <div>
<div ref="chart"></div>
</div>;
},
componentDidMount: function() {
var chartDOMNode = this.refs['chart'].getDOMNode();
var m = 7; // number of samples per layer
var margin = {top: 4, right: 0, bottom: 10, left: 0},
that = this;
this.width = chartDOMNode.clientWidth - margin.left - margin.right;
this.height = chartDOMNode.clientHeight - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([39, this.width], 0.85, 0);
this.y = d3.scale.linear()
.domain([0, 0])
.range([this.height, 0]);
this.yAxis = d3.svg.axis()
.scale(this.y)
.orient('left')
.tickSize(-this.width, 0)
.tickFormat(function (y, num) {
return y !== 0 && num < maxTicks ? Math.round(y * 10) / 10 + '%' : null;
});
this.line = d3.svg.line()
.x(function (d) {
return x(d.x);
})
.y(function (d) {
return that.y(d.y);
});
this.svg = d3.select(chartDOMNode).append('svg')
.attr('width', this.width + margin.left + margin.right)
.attr('height', this.height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
this.svg.append('g')
.attr('class', 'y axis')
.call(this.yAxis);
var emptySeries = _.map(_.range(m), function (num) {
return {x: num, y: 0};
});
this.layer = this.svg.selectAll('.layer')
.data([emptySeries, emptySeries])
.enter()
.append('g')
.attr('class', 'layer');
this.layer.append('path')
.attr('fill', 'none')
.attr('stroke-width', '3')
.attr('stroke', function (d, j) {
return colors[j];
})
.attr('class', 'line')
.attr('d', this.line);
this.layer.selectAll('circle')
.data(function (d) {
return d;
})
.enter().append('circle')
.attr('cx', function (d) {
return x(d.x);
})
.attr('cy', function (d) {
return that.y(d.y);
})
.attr('r', 6)
.attr('fill', function (d, i, j) {
return colors[j];
})
.attr('stroke', 'white')
.attr('stroke-width', 3);
}
render: function () {
var cn = this.className;
/* jshint ignore:start */
var paths = prepareData(this.state.series);
return <div onClick={this.handleClickUpdate}>
<svg width={width} height={height}>
<g>
{_.map(paths, function (paths, index) {
return <g key={index}>
{paths.series && <path {...paths.series} />}
<g style={paths.series.style}>
{_.map(paths.points, function (point, pointIndex) {
return <path key={pointIndex} {...point} />;
})}
</g>
</g>;
}, this)}
</g>
</svg>
</div>;
/* jshint ignore:end */
}
Common
- D3 - calculations
- D3 - rendering
- React - stupid wrapper
Wise
- D3 - calculations
- React - rendering
?
React in practice
By Alexey Volkov
React in practice
- 498