@dan_abramov
Front-end at Stampsy
Hacker News on Dropbox
V8 benchmark suite v7
I want to use the same component in a number of places, with slightly different display options.
Collection
Share
FormInputRow
I want to express components in terms of other components.
Collection
UserImage
UserLink
FollowButton
UserLink
UserImage
LikeButton
Collection
Share
InfoPanel
Components are not static; their state changes over their lifetime.
state might come from data
state might come from user input
... sometimes at 60fps
(e.g. opacity changing on scroll)
<p id="bottles">9 bottles of beer on the wall</p>
<button id="take">Take one down and pass it around</button>
<button id="again">Start again</button>
var para = $('#bottles'),
take = $('#take'),
again = $('#again'),
bottles = 99;
function update() {
if (bottles > 0) {
take.show();
again.hide();
} else {
take.hide();
again.show();
}
if (bottles > 1) {
para.text(bottles + ' bottles of beer on the wall');
} else if (bottles === 1) {
para.text('One bottle of beer on the wall');
} else {
para.text('No bottles of beer on the wall');
}
}
take.click(function () {
bottles--;
update();
});
again.click(function () {
bottles = 99;
update()
});
update();
var BottleGameView = Backbone.View.extend({
el: '#container',
events: {
'click button.take': 'takeBottle',
'click button.again': 'startAgain'
},
initialize: function () {
this.model = new Backbone.Model({ bottles: 99 });
this.render();
this.listenTo(this.model, 'change:bottles', this.handleBottlesChange);
},
handleBottlesChange: function () {
var bottles = this.model.get('bottles');
if (bottles > 0) {
this.$take.show();
this.$again.hide();
} else {
this.$take.hide();
this.$again.show();
}
if (bottles > 1) {
this.$para.text(bottles + ' bottles of beer on the wall');
} else if (bottles === 1) {
this.$para.text('One bottle of beer on the wall');
} else {
this.$para.text('No bottles of beer on the wall');
}
},
render: function () {
var template = _.template($('#bottles_template').html(), this.model.attributes);
this.$el.html(template);
this.$para = this.$el.find('.bottles');
this.$take = this.$el.find('.take');
this.$again = this.$el.find('.again');
this.$again.hide();
},
takeBottle: function () {
this.model.set('bottles', this.model.get('bottles') - 1);
},
startAgain: function () {
this.model.set('bottles', 99);
}
});
new BottleGameView();
var BottleGame = React.createClass({
getInitialState: function () {
return {
bottles: 99
};
},
render: function() {
var bottles = this.state.bottles,
text;
if (bottles > 1) {
text = bottles + ' bottles of beer on the wall';
} else if (bottles === 1) {
text = 'One bottle of beer on the wall';
} else {
text = 'No bottles of beer on the wall';
}
return React.DOM.div(null,
React.DOM.p(null, text),
bottles > 0 ?
React.DOM.button({ onClick: this.handleTakeClick }, 'Take one down and pass it around') :
React.DOM.button({ onClick: this.handleAgainClick }, 'Start again')
);
},
handleTakeClick: function () {
this.setState({ bottles: this.state.bottles - 1 });
},
handleAgainClick: function () {
this.setState({ bottles: 99 });
}
});
React.renderComponent(BottleGame(), document.body);
You: (props, state) -> virtual DOM
React: (virtual DOM, prev virtual DOM) -> DOM operations
return div(null,
p(null, text),
bottles > 0 ?
button({ onClick: this.handleTakeClick }, 'Take one down and pass it around') :
button({ onClick: this.handleAgainClick }, 'Start again')
);
can be rewritten with an optional preprocessor called JSX as
return (
<div>
<p>{text}</p>
{bottles > 0 ?
<button onClick={this.handleTakeClick}>Take one down and pass it around</button> :
<button onClick={this.handleAgainClick}>Start again</button>
}
</div>
);
Components accept props and can't mutate them. Props can be used to pass data or display options.
var Title = React.createClass({
propTypes: {
gender: React.PropTypes.oneOf(['male', 'female']).required
},
render: function () {
return (this.props.gender === 'male') ?
<span>Mr.</span> :
<span>Ms.</span>;
}
});
// <span>Mr.</span>
React.renderComponent(<Title gender='male' />, document.body);
// <span>Ms.</span>
React.renderComponent(<Title gender='female' />, document.body);
Components are expressed in terms of other components.
var FormalName = React.createClass({
propTypes: {
name: React.PropTypes.string,
gender: React.PropTypes.oneOf(['male', 'female']).required
},
render: function () {
return (
<div>
<Title gender={this.props.gender} /> {this.props.name}
</div>
);
}
});
// <span>Mr.</span> Ken
React.renderComponent(<FormalName gender='male' name='Ken' />, document.body);
// <span>Ms.</span> Barbie
React.renderComponent(<FormalName gender='female' name='Barbie' />, document.body);
Components also get a special “children” prop.
var Link = React.createClass({
propTypes: {
to: React.PropTypes.string.isRequired
},
render: function () {
return (
<a href={this.props.to} onClick={this.handleClick}>
{this.props.children}
</a>
);
},
handleClick: function () {
router.navigate(this.props.to);
}
});
// <a href='/user/3'><span>Mr.</span> Ken</a> with a click handler
React.renderComponent(
<Link to='user/3'>
<FormalName gender='male' name='Ken' />
</Link>,
document.body
);
The only mutable part in component is its private state.
var BottleGame = React.createClass({
getInitialState: function () {
return {
bottles: 99
};
},
render: function() {
var bottles = this.state.bottles,
text = this.getText(bottles);
return (
<div>
<p>{text}</p>
{bottles > 0 ?
<button onClick={this.handleTakeClick}>Take one down and pass it around</button> :
<button onClick={this.handleAgainClick}>Start again</button>
}
</div>
);
},
handleTakeClick: function () {
this.setState({ bottles: this.state.bottles - 1 });
},
handleAgainClick: function () {
this.setState({ bottles: 99 });
}
});
React.renderComponent(BottleGame(), document.body);
Our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.
render: function () {
return (
<div className='LikeButton'>
<Button size={this.props.size}
onClick={this.handleLikeClick}
active={this.props.isLiked}
round
like>
<Icon name='like'
size='small'
mode='stroke' />
</Button>
{this.props.showCounter &&
<span className='LikeButton-likeCount'>
{this.props.likeCount}
</span>
}
{this.props.showLikers &&
<ul className='LikeButton-likers'>
{this.renderLikers()}
</ul>
}
</div>
);
}
<LikeButton likeCount={24} isLiked={false} />
// will become HTML:
<div class="LikeButton LikeButton--extraLarge">
<a data-active="true" class="Btn Btn--like Btn--round">
<div class="Icon Icon--small Icon--like Icon--stroke">
<span class="Icon-container">
<svg viewBox="0 0 18 18">
<path stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M9 16c4.25-3.26 8-5.15 8-9.68C17 3.67 14.92 2 12.86 2 11.27 2 9.66 2.87 9 4.69 8.33 2.87 6.73 2 5.14 2 3.08 2 1 3.67 1 6.32 1 10.85 4.75 12.74 9 16z"></path>
</svg>
</span>
</div>
</a>
<span class="LikeButton-likeCount">
24
</span>
</div>
LikeButton.jsx
Output example
MVC has its problems though.
What exactly happens when I call .set()?
* only one small module is released as it's useful in any Flux app
Not CQRS
CQRS
Stores hold Truth
If data is wrong, you know whose fault it is
Data mutation is contained to Stores.
Think that each store is like a database table (although more flexible).
If your Stores contain duplicate data, you're doing it wrong.
How do they know what happened?
{
type: ActionTypes.FOLLOW_ZINE,
zineSlug: 'lightbox-magazine'
}
{
type: 'LIKE_STAMP',
stampId: 1024
}
{
type: 'SHARE_STAMP',
stampId: 1024,
service: 'facebook'
}
handleFollowClick: function () {
var zine = this.state.zine;
if (zine.isFollowing) {
AppDispatcher.handleAction({
type: ActionTypes.FOLLOW_ZINE,
zineSlug: zine.slug
});
} else {
AppDispatcher.handleAction({
type: ActionTypes.UNFOLLOW_ZINE,
zineSlug: zine.slug
});
}
var zines = {};
var CHANGE_EVENT = 'change';
var ZineStore = merge(EventEmitter.prototype, {
getZine: function(slug) {
return zines[slug];
},
emitChange: function () {
this.emit(CHANGE_EVENT);
},
addChangeListener: function (callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function (callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.FOLLOW_ZINE:
zines[action.zineSlug].isFollowed = true;
zines[action.zineSlug].followerCount++;
ZineStore.emitChange();
}
});
module.exports = ZineStore;
var FollowButton = React.createClass({
componentDidMount: function () {
ZineStore.addChangeListener(this.handleChange);
this.handleChange();
},
componentWillUnmount: function () {
ZineStore.removeChangeListener(this.handleChange);
},
handleChange: function () {
this.setState({
zine: ZineStore.get(this.props.zineSlug)
});
},
render: function () {
// ...
}
});