This is more of a \"whats your opinion/Am I correct in thinking this?\" question.
Trying to be as strict as possible while understanding Flux, I was trying
A few differences in my implementation:
I like stores employing a flyweight pattern. That is, unless forced to, all operations are "getOrRetrieveOrCreate"
I've had to forgo promise heavy development in favor of events/state. Async communication should still use promises, that is, things in actions use them otherwise communication occurs using events. If a view always renders the current state, then you need a state like "isLoading" to render a spinner. Or you need an event to get fired then update a state on a view. I think responding from an action with a promise may be an anti-pattern (not entirely sure).
URL changes fire the appropriate action. GET should work and be idempotent so a URL change should generally not result in a failure. It may however result in a redirect. I have an "authRequired" decorator for some actions. If you aren't authenticated then we redirect you to the login page with the target URL listed as a redirect path.
For validation we are thinking about starting from an action, firing a "xyzModel:willSaveData", before we start; then firing either "xyzModel:didSaveData" or "xyzModel:failedSaveData" events. The store listening to these events will indicate "saving" to the views that care. It may also indicate "hasValidationError" to views that care. If you want to dismiss an error. You can fire an action from a view that indicates that the error "wasReceived", which removes the "hasValidationError" flag or optionally could do something else like clear out all validation errors. Validations are interesting because of the different styles of validation. Ideally, you could create an app that would accept most any input due the limitations imposed by your input elements. Then again, servers may disagree with those choices :/.
It's my implementation using facebook Flux and Immutable.js that I think responds to many of your concerns, based on few rules of thumb :
STORES
Record instance via ids.WebAPIUtils for read operations and trigger actions for write operations.RecordA and FooRecordB are resolved from a RecordA instance through a foo_id params and retrieved via a call such as FooStore.get(this.foo_id)getters methods such as get(id), getAll(), etc.APIUTILS
PromisePromise indexed by the hash of url + paramsPromise is resolved or rejected.fooError action should certainly contains payloads with validation errors returned by the server.COMPONENTS
Perf.printWasted time, it should be very low, few ms.props as explicit as possible via propsType.var fooRecord = { foo:1, bar: 2, baz: 3}; (I'm not using Immutable.Record here for the sake of simplicity of this example) and my child component need to display fooRecord.foo and fooRecord.bar, I do not pass the entire foo object but only fooRecordFoo and fooRecordBar as props to my child component because an other component could edit the foo.baz value, making the child component re-render while this component doesn't need at all this value !ROUTING - I simply use ReactRouter
Here is a basic example :
api
apiUtils/Request.js
var request = require('superagent');
//based on http://stackoverflow.com/a/7616484/1836434
var hashUrl = function(url, params) {
var string = url + JSON.stringify(params);
var hash = 0, i, chr, len;
if (string.length == 0) return hash;
for (i = 0, len = string.length; i < len; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
var _promises = {};
module.exports = {
get: function(url, params) {
var params = params || {};
var hash = hashUrl(url, params);
var promise = _promises[hash];
if (promise == undefined) {
promise = new Promise(function(resolve, reject) {
request.get(url).query(params).end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
_promises[hash] = promise;
}
return promise;
},
post: function(url, data) {
return new Promise(function(resolve, reject) {
var req = request
.post(url)
.send(data)
.end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
};
apiUtils/FooAPI.js
var Request = require('./Request');
var FooActionCreators = require('../actions/FooActionCreators');
var _endpoint = 'http://localhost:8888/api/foos/';
module.exports = {
getAll: function() {
FooActionCreators.receiveAllPending();
Request.get(_endpoint).then( function(res) {
FooActionCreators.receiveAllSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveAllError(err);
});
},
get: function(id) {
FooActionCreators.receivePending();
Request.get(_endpoint + id+'/').then( function(res) {
FooActionCreators.receiveSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveError(err);
});
},
post: function(fooData) {
FooActionCreators.savePending();
Request.post(_endpoint, fooData).then (function(res) {
if (res.badRequest) { //i.e response return code 400 due to validation errors for example
FooActionCreators.saveInvalidated(res.body);
}
FooActionCreators.saved(res.body);
}).catch( function(err) { //server errors
FooActionCreators.savedError(err);
});
}
//others foos relative endpoints helper methods...
};
stores
stores/BarStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../APIUtils/BarAPI')
var CHANGE_EVENT = 'change';
var _bars = Immutable.OrderedMap();
class Bar extends Immutable.Record({
'id': undefined,
'name': undefined,
'description': undefined,
}) {
isReady() {
return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
}
getBar() {
return BarStore.get(this.bar_id);
}
}
function _rehydrate(barId, field, value) {
//Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
_bars = _bars.updateIn([barId, field], function() {
return value;
});
}
var BarStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_bars.has(id)) {
BarAPI.get(id);
return new Bar(); //we return an empty Bar record for consistency
}
return _bars.get(id)
},
getAll: function() {
return _bars.toList() //we want to get rid of keys and just keep the values
},
Bar: Bar,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
var _setBar = function(barData) {
_bars = _bars.set(barData.id, new Bar(barData));
};
var _setBars = function(barList) {
barList.forEach(function (barData) {
_setbar(barData);
});
};
BarStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.BAR_LIST_RECEIVED_SUCESS:
_setBars(action.barList);
BarStore.emitChange();
break;
case ActionTypes.BAR_RECEIVED_SUCCESS:
_setBar(action.bar);
BarStore.emitChange();
break;
case ActionTypes.BAR_REHYDRATED:
_rehydrate(
action.barId,
action.field,
action.value
);
BarStore.emitChange();
break;
}
});
module.exports = BarStore;
stores/FooStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/FooConstants').ActionTypes;
var BarStore = require('./BarStore');
var FooAPI = require('../APIUtils/FooAPI')
var CHANGE_EVENT = 'change';
var _foos = Immutable.OrderedMap();
class Foo extends Immutable.Record({
'id': undefined,
'bar_id': undefined, //relation to Bar record
'baz': undefined,
}) {
isReady() {
return this.id != undefined;
}
getBar() {
// The whole point to store an id reference to Bar
// is to delegate the Bar retrieval to the BarStore,
// if the BarStore does not have this Bar object in
// its cache, the BarStore will trigger a GET request
return BarStore.get(this.bar_id);
}
}
function _rehydrate(fooId, field, value) {
_foos = _foos.updateIn([voucherId, field], function() {
return value;
});
}
var _setFoo = function(fooData) {
_foos = _foos.set(fooData.id, new Foo(fooData));
};
var _setFoos = function(fooList) {
fooList.forEach(function (foo) {
_setFoo(foo);
});
};
var FooStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_foos.has(id)) {
FooAPI.get(id);
return new Foo();
}
return _foos.get(id)
},
getAll: function() {
if (_foos.size == 0) {
FooAPI.getAll();
}
return _foos.toList()
},
Foo: Foo,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
FooStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.FOO_LIST_RECEIVED_SUCCESS:
_setFoos(action.fooList);
FooStore.emitChange();
break;
case ActionTypes.FOO_RECEIVED_SUCCESS:
_setFoo(action.foo);
FooStore.emitChange();
break;
case ActionTypes.FOO_REHYDRATED:
_rehydrate(
action.fooId,
action.field,
action.value
);
FooStore.emitChange();
break;
}
});
module.exports = FooStore;
components
components/BarList.react.js (controller-view component)
var React = require('react/addons');
var Immutable = require('immutable');
var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
barList: BarStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
BarStore.removeChangeListener(this._onChange);
},
render: function() {
var barItems = this.state.barList.toJS().map(function (bar) {
// We could pass the entire Bar object here
// but I tend to keep the component not tightly coupled
// with store data, the BarItem can be seen as a standalone
// component that only need specific data
return <BarItem
key={bar.get('id')}
id={bar.get('id')}
name={bar.get('name')}
description={bar.get('description')}/>
});
if (barItems.length == 0) {
return (
<p>Loading...</p>
)
}
return (
<div>
{barItems}
</div>
)
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
components/BarListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
// I use propTypes to explicitly telling
// what data this component need. This
// component is a standalone component
// and we could have passed an entire
// object such as {id: ..., name, ..., description, ...}
// since we use all the datas (and when we use all the data it's
// a better approach since we don't want to write dozens of propTypes)
// but let's do that for the example's sake
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
}
render: function() {
return (
<li>
<p>{this.props.id}</p>
<p>{this.props.name}</p>
<p>{this.props.description}</p>
</li>
)
}
});
components/BarDetail.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
var BarActionCreators = require('../actions/BarActionCreators');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
},
handleSubmit: function(event) {
//Since we keep the Bar data up to date with user input
//we can simply save the actual object in Store.
//If the user goes back without saving, we could display a
//"Warning : item not saved"
BarActionCreators.save(this.props.id);
},
handleChange: function(event) {
BarActionCreators.rehydrate(
this.props.id,
event.target.name, //the field we want to rehydrate
event.target.value //the updated value
);
},
render: function() {
return (
<form onSubmit={this.handleSumit}>
<input
type="text"
name="name"
value={this.props.name}
onChange={this.handleChange}/>
<textarea
name="description"
value={this.props.description}
onChange={this.handleChange}/>
<input
type="submit"
defaultValue="Submit"/>
</form>
)
},
});
components/FooList.react.js (controller-view component)
var React = require('react/addons');
var FooStore = require('../stores/FooStore');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
fooList: FooStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
FooStore.addChangeListener(this._onChange);
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
FooStore.removeChangeListener(this._onChange);
BarStore.removeChangeListener(this._onChange);
},
render: function() {
if (this.state.fooList.size == 0) {
return <p>Loading...</p>
}
return this.state.fooList.toJS().map(function (foo) {
<FooListItem
fooId={foo.get('id')}
fooBar={foo.getBar()}
fooBaz={foo.get('baz')}/>
});
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
components/FooListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Bar = require('../stores/BarStore').Bar;
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
fooId: React.PropTypes.number.isRequired,
fooBar: React.PropTypes.instanceOf(Bar).isRequired,
fooBaz: React.PropTypes.string.isRequired
}
render: function() {
//we could (should) use a component here but this answer is already too long...
var bar = <p>Loading...</p>;
if (bar.isReady()) {
bar = (
<div>
<p>{bar.get('name')}</p>
<p>{bar.get('description')}</p>
</div>
);
}
return (
<div>
<p>{this.props.fooId}</p>
<p>{this.props.fooBaz}</p>
{bar}
</div>
)
},
});
Let's go through an entire loop for FooList:
FooListcontroller-view componentFooListcontroller-view component calls FooStore.getAll()_foos map is empty in FooStore so FooStore performs a request via FooAPI.getAll()FooList controller-view component renders itself as loading state since its state.fooList.size == 0.Here's the actual look of our list :
++++++++++++++++++++++++
+ +
+ "loading..." +
+ +
++++++++++++++++++++++++
FooAPI.getAll() request resolves and triggers the FooActionCreators.receiveAllSuccess actionFooStore receive this action, updates its internal state, and emits change.FooList controller-view component receive change event and update its state to get the list from the FooStorethis.state.fooList.size is no longer == 0 so the list can actually renders itself (note that we use toJS() to explicitly get a raw javascript object since React does not handle correctly mapping on not raw object yet).FooListItem component.foo.getBar() we're telling to the FooStore that we want the Bar record back.getBar() method of Foo record retrieve the Bar record through the BarStoreBarStore does not have this Bar record in its _bars cache, so it triggers a request through BarAPI to retrieve it.Foo in this.sate.fooList of FooList controller-view component++++++++++++++++++++++++ + + + Foo1 "name1" + + Foo1 "baz1" + + Foo1 bar: + + "loading..." + + + + Foo2 "name2" + + Foo2 "baz2" + + Foo2 bar: + + "loading..." + + + + Foo3 "name3" + + Foo3 "baz3" + + Foo3 bar: + + "loading..." + + + ++++++++++++++++++++++++
-Now let's say the BarAPI.get(2) (requested by Foo2) resolves before BarAPI.get(1) (request by Foo1). Since it's asynchronous it's totally plausible.
- The BarAPI triggers the BAR_RECEIVED_SUCCESS' action via theBarActionCreators.
- TheBarStore` responds to this action by updating its internal store and emits change. That's the now the fun part...
FooList controller-view component responds to the BarStore change by updating its state.render method is calledfoo.getBar() call now retrieve a real Bar record from BarStore. Since this Bar record has been effectively retrieved, the ImmutablePureRenderMixin will compare old props with current props and determine that the Bar objects has changed ! Bingo, we could re-render the FooListItem component (a better approach here would be to create a separate FooListBarDetail component to let only this component to re-render, here we also re-rendering the Foo's details that have not changed but for the sake of simplicity let's just do that).++++++++++++++++++++++++ + + + Foo1 "name1" + + Foo1 "baz1" + + Foo1 bar: + + "loading..." + + + + Foo2 "name2" + + Foo2 "baz2" + + Foo2 bar: + + "bar name" + + "bar description" + + + + Foo3 "name3" + + Foo3 "baz3" + + Foo3 bar: + + "loading..." + + + ++++++++++++++++++++++++
If you want me to add more details from a non detailed part (such as action creators, constants, routing, etc., use of BarListDetail component with form, POST, etc.) just tell me in the comments :).