How I Learned to Stop Worrying And Love The Flux
j@ideadevice.com
@jaseemabid
Hello!
Demo!
Why not MVC?
- This talk is about how we did it.
- Take away what you want, innovate.
Opinionated
- Messages that go through the system
- Type and Data
Actions
{
"type": "NEW_MESSAGE",
"data": {
"id": 42,
"message": "Hello Meta Refresh"
}
}
Dispatcher
-
Central broker that sends actions to registered stores
-
We went with Facebook's own implementation
Another pub sub?
-
Callbacks are not subscribed to particular events
-
Every payload is dispatched to every registered callback
-
Callbacks can be deferred
...
Dispatcher.register(function(payload) {
if (payload.type === 'NEW_MESSAGE') {
console.log(payload.message);
}
});
...
...
// On some events: Web Sockets, API Responses, etc.
Dispatcher.dispatch({
type: 'NEW_MESSAGE',
message: 'Hello Meta Refresh'
});
...
Stores
- Thin wrapper around Immutable.js
- Holds data as well as state
- Stateful, but not Asynchronous
- Listens to Actions
- Emits changes from action payloads
- ItemStore and IndexStore
class Store {
constructor(spec, dispatchToken) {
this.dispatchToken = dispatchToken;
assign(this, EventEmitter.prototype, spec);
this.init();
}
emitChange() {
this.emit('change');
}
addChangeListener(callback) {
this.on('change', callback);
}
removeChangeListener(callback) {
this.removeListener('change', callback);
}
}
var UserStore = new Store({
getAll: function() {
if (Object.keys(this._items).length === 0) {
this._items = {
state: 'PENDING'
};
UserActions.get();
}
return this._items;
},
getByID: function(id) {
if (!this._items[id]) {
this._items[id] = {
state: 'PENDING'
};
UserActions.get(id);
}
return this._items[id];
}
}, function () { ... });
var UserStore = new Store({
...
}, Dispatcher.register(payload) {
var type = payload.type,
users = payload.users;
switch(type) {
case 'USER_GET_SUCCESS':
users.forEach(function(user) {
this._items[user.id] = user;
}.bind(this));
this.emitChange();
break;
case 'USER_GET_FAILURE':
this._items = {
state: 'FAILURE'
};
this.emitChange();
break;
};
});
UserActions = {
get: function(id) {
Dispatcher.dispatch({
type: 'USER_GET_PENDING'
});
// API Wrapper
getUsers(id, function(err, users) {
if (err) {
Dispatcher.dispatch({
type: 'USER_GET_FAILURE',
error: err
});
} else {
Dispatcher.dispatch({
type: 'USER_GET_SUCCESS',
users: users
});
}
});
}
};
A lot more on stores
-
Not just models and collections
-
Listens and act on actions
-
Stores don't mutate themselves
-
Singletons
-
May talk to other stores, but we avoided it
- React
- Almost stateless
- Synchronous. No XHR, no promises, no callbacks
- Views get data from stores and listen for changes
- render :: State → HTML
Views
var UserList = React.createClass({
componentWillMount: function() {},
componentDidUpdate: function() {},
getInitialState: function() {},
getStateFromStores: function() {},
setStateFromStores: function() {},
render: function() {}
});
...
getInitialState: function() {
return {
users: this.getStateFromStores()
};
},
getStateFromStores: function() {
return UserListStore.getAll();
},
setStateFromStores: function() {
this.setState({
users: this.getStateFromStores()
});
}
...
...
componentDidMount: function() {
UserIndexStore.addChangeListener(
this.setStateFromStores);
},
componentWillUnmount: function() {
UserIndexStore.removeChangeListener(
this.setStateFromStores);
}
...
...
render: function() {
var users = this.state.users;
return (
<table>
{users.map(function(user) {
return (
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
);
})}
</table>
);
}
...
!
.
this.state & this.props
var UserList = React.createClass({
...
onSubmit: function(e) {
e.preventDefault();
if (this.state.isNew) {
UserActions.create(
this.state.user.toJSON());
} else {
UserActions.update(
this.state.user.toJSON());
}
}
...
});
UserActions = {
...
create: function(user) {
Dispatcher.dispatch({
type: 'USER_CREATE',
message: 'Creating User "' + user.username + '"',
item: user
});
// The async bit
createUser(user).then(function(data) {
Dispatcher.dispatch({
type: 'USER_CREATE_SUCCESS',
item: user,
data: data
});
}, function(err) {
Dispatcher.dispatch({
type: 'USER_CREATE_FAILURE',
item: user,
error: err
});
});
}
}
Dependent stores
-
Data dependency between stores
-
IndexStore `waitFor` ItemStore
class IndexStore extends Store {
constructor(ItemStore, spec, dispatcherCallback) {
super(spec, Dispatcher.register(function(payload) {
if (payload.action.type === this.GET_SUCCESS_ACTION) {
// Wait for the related item stores to finish
Dispatcher.waitFor([ItemStore.dispatchToken]);
...
Testing
- React.addons.TestUtils
- Mocha vs Jest
describe('User list testing', () => {
it('Renders a loading page', () => {
// Render the Expiry tab into the document
var userlist = TestUtils.renderIntoDocument('<UserList/>'),
node = expiry.getDOMNode();
expect(node.textContent).to.be("LOADING");
});
it('Shows atlest one user', () => {
// Fire user get action
Dispatcher.fire({
type: 'USER_GET_SUCCESS',
data: [{
name: 'John Doe'
}]
});
expect(node.textContent).to.be(...);
}):
});
Some lessons
- Fewer network requests in certain cases
- Testable to a large extend
- Asynchronous code localized to action creators
- State localized to stores mostly
- Views mostly stateless, synchronous
- Playbacks with actions
- Detailed analytics
There is a lot more...
- Optimistic UI updates
- Excellent caching
- Immutable.js backed actions and stores
- React.js views
- hasEnoughDetail
- Error handling
- Awesome routing
flux
By jaseemabid
flux
- 1,016