Components, React and Flux
@dan_abramov
Front-end at Stampsy
No Silver Bullet
- http://youmightnotneedjquery.com
- You Might Not Need React Either, But...
Give It Five Minutes
Hacker News on Dropbox
Build a single-page app, they said
Modern browsers: good at some things
rendering a complex layout
fetching stuff
running JS crazy fast
V8 benchmark suite v7
natives are faster.. not
- http://stackoverflow.com/questions/18881487/why-is-lodash-each-faster-than-native-foreach
- Builtins adhere to ridiculous semantic complexity
- JITed code is as native as you can get
Surely this technology will help me build a single-page app?
I only need a few things!
autocomplete
modal
paginated list
notification
form
reusable,
composable,
stateful
components
reusable
I want to use the same component in a number of places, with slightly different display options.
Collection
Share
FormInputRow
composable
I want to express components in terms of other components.
Collection
UserImage
UserLink
FollowButton
UserLink
UserImage
LikeButton
Collection
Share
InfoPanel
stateful
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)
I don't need a widget library.
I need a sane way to build composable stateful interfaces with minimal mental overhead.
What I Tried
HTML + some jQuery
- No nesting
- Where's the data?
- When data changes, how do I update things?
- Doesn't scale
<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();
- First rendering and updates may not be consistent;
- `update` complexity depends on how radical state changes may be.
Backbone + Templates
- Too flexible when rigid is better (e.g. view lifecycle)
- Too rigid when flexible is better (e.g. dynamic nesting is too hard)
- Must decide what's dynamic beforehand
- Scales but taxes your designs with compromises
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();
- First rendering and updates may not be consistent;
- Complexity still depends on how radical state changes may be;
- Enforces some structure around what we were doing with jQuery but doesn't offer anything else in terms of updating the DOM.
React
- Components describe their DOM at any point of time
- React takes care of updating the actual DOM
- Very small API surface (5 methods)
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
- State is explicit
- No conceptual gap between first and subsequent renders;
- React guarantees consistency
- You describe how DOM looks, React takes care of updating it
- Virtual DOM is an implementation detail
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>
);
#jsxreactions
- You don't have to use it
- No funky special syntax (Mustache, Angular), just JS
- It's easier on eyes for describing DOM trees than parens
- It's a simple syntactic transform, not React-specific
- It's trivial to add to your Gulp, Grunt or other workflow
- Designers are comfortable with it
- You might like it, too, after you actually use it
reusable
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);
composable
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
);
stateful
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.
-
Async is hard
-
State is hard
-
We're stupid
-
Plan accordingly
components
- Something like BEM for CSS
- Common sense, not religious
- Designers create components, developers add handlers, or vice versa
- Conceptually we divide components into “atoms”, “molecules” and “organisms“ (Atomic Design)
- React infects your codebase in a good way
- Composition and mixins over inheritance
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
unidirectional data flow
- Most important React principle
- More important than JSX or virtual DOM
- Two-way bindings are the root of all evil (or is it in another direction? we don't know!)
- Single source of truth
- Data flows from top components to bottom components
- React Developer Tools
- Move state up the tree!
- Keep components stupid!
- When child needs to be able to change parent's data, parent gives it a callback (there are better solutions though!)
Demo: inspecting Stampsy with React Dev Tools
fast
- Performance is not an afterthought or compromise
- No watching or dirty-checking objects
- DOM updates are slow, not JS itself
- React does minimal DOM updates possible
- React batches reads and writes, it's hard to do this manually
- Provides hooks for custom update logic (almost never needed)
- Can do this all at 60fps, even in non-JIT UIWebView
a lot like Doom 3 engine
Demo: 60fps opacity change on scroll
tooling
- Works with or without a bundler, community prefers Webpack
- Can integrate in any stack, and you can start small as we did
- Can work with any router but react-router de facto won
- JSX transform supported with Gulp, Grunt, Node
- JSX syntax highlighting in Sublime, Atom, other editors
- JSX can be easily linted
- Stable, FB uses it in all new UI code for several years, Yahoo, Airbnb, Reddit and other folks switching too
Demo: hinting JSX
advanced
- shouldComponentUpdate
- Experiments in getting rid of CSS
- react-raf-batching
- It is possible to hot-reload components as you edit them
- Om is fine-tuned React
- react-multiplayer
Demo: hot reloading
architecture
- React is unopinionated, you can fetch data with $.ajax(), Backbone, whatever you fancy
- Can easily be hooked up to Backbone or other MVC and react to changes
- We lived in a mixed React+Backbone world for months and they worked seamlessly
MVC has its problems though.
What's wrong with MVC?
What exactly happens when I call .set()?
-
Multiple copies of data
-
Nested entities
Flux brings unidirectional flow to data changes
It's like a more centralized MVC playing on React's strengths
(but not really)
architecture, not framework*
* only one small module is released as it's useful in any Flux app
Give It Five Minutes
-
extrapolated from successful rewrite of Facebook chat
-
battle-tested on many Facebook production projects, went through several dogfooding iterations
-
minor differences in flavors
-
nothing new under the sun
similar to CQRS
Not CQRS
CQRS
When reads and writes get more sophisticated, they don't fit under a single “model” umbrella
- sophisticated writes: validation, updating related entities, counters
- sophisticated reads: aggregation, grouping
Separating reads and writes
Stores hold Truth
If data is wrong, you know whose fault it is
Data mutation is contained to Stores.
Data is normalized inside Stores
Think that each store is like a database table (although more flexible).
If your Stores contain duplicate data, you're doing it wrong.
Stores have no setters
How do they know what happened?
actions
- Represent everything user can do in an app
- Plain JS objects with a "type" property
- "type" is one of ActionTypes, string constants defined by your app
{
type: ActionTypes.FOLLOW_ZINE,
zineSlug: 'lightbox-magazine'
}
{
type: 'LIKE_STAMP',
stampId: 1024
}
{
type: 'SHARE_STAMP',
stampId: 1024,
service: 'facebook'
}
components express their intent by firing actions on Dispatcher
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
});
}
stores listen to Dispatcher
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;
components grab their state from stores
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 () {
// ...
}
});
xhr responses are also actions
- FOLLOW_ZINE_SUCCESS
- LIKE_STAMP_ERROR
- RECEIVE_FEED_PAGE
benefits
- Single source of truth
- No data duplication
- All actions are visible
- Updates never cascade
- Many stores can react to same action
- Optimistic and pessimistic updates
- Cache data aggresively
- Record and replay bugs
- Pre-specify any actions from server
- Test components and stores separately
Demo:
- Extensive caching
- Optimistic updates
- All actions are visible
more on flux
- http://facebook.github.io/flux/
- https://github.com/gaearon/flux-react-router-example
- Not panacea
- Still, very solid if you have dynamic UI with changing data
Components, React and Flux (WIP)
By Dan Abramov
Components, React and Flux (WIP)
- 85,405