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 to figure out where XHR calls are made, websockets/external stimuli handled, routing takes places, etc.
From what I read across articles, interviews and looking through facebook examples there are a few ways of handling these things. Following flux strictly, Action creators are the ones that do all the XHR calls with the possibility of a PENDING/SUCCESS/FAILURE Actions being fired before and after the request completes.
Another was, coming from facebook's Ian Obermiller, all the READ(GETs) requests are handled directly by the Stores(without involvement of an Action creator/dispatcher) and WRITE(POSTs) requests are handled by the Action Creators going through the entire action>dispatcher>store flow.
Some understandings/conclusions we drew/would like to stick to:
Ideally, anything going in/out of the system happens only through Actions.
Async calls leaving/entering the system will have PENDING/PROGRESS(think file uploads)/SUCCESS/FAILURE Actions.
Single dispatcher across the entire App.
Action>Dispatcher>Store calls are strictly synchronous to stick to the dispatches not being able to start another dispatch internally to avoid chaining events/actions.
Stores are persisted across Views(considering its a single page app, you want to be able to reuse data)
A few questions that we came to some conclusion with, but I'm not entirely satisfied with:
If you take the approach where Stores do Reads, and Actions to Writes, how do you handle situations where multiple Stores might be able to use data from a single XHR call?
Example: API calls issued by TeamStore to /api/teams/{id} which returns something like:
{
entities: {
teams: [{
name: ...,
description: ...,
members: [1, 2, 4],
version: ...
}],
users: [{
id: 1
name: ...,
role: ...,
version: ...
},
{
id: 2
name: ...,
role: ...,
version: ...
},
{
id: 3
name: ...,
role: ...,
version: ...
}]
}
}
Ideally, I'd also like to update the MemberStore with the information returned in this API. We maintain a version number for every entity which is updated on updates to the record, which is what we use internally do reject calls on stale data, etc. Using this, I could have an internal logic, where if I as a side effect of some other API call, I know my data is stale, I trigger a refresh on that record.
The solution, it would seem, is that you'd need the store to trigger an action(which would effectively update the other dependent stores). This short circuits the Store>View>Action to Store>Action and I'm not sure if its a good idea. We already have one thing out of sync with Stores doing their own XHR calls. Concessions like these would start creeping into the entire system eventually.
Or Stores that are aware of other stores and be able to communicate with them. But this breaks the Stores have no Setters rule.
A simple solution to the above problem would be that you stick to Actions being the ONLY place external incoming/outgoing stimulus happens. This simplifies the logic of multiple Stores getting updated.
But now, where and how do you handle caching? We came to the conclusion that the caching would happen at the API Utils/DAO level. (if you look at the flux diagram).
But this introduces other problems. To better understand/explain what I mean by example:
/api/teams returns a list of all the teams with which I display a list of all the teams.
On clicking on a team's link, I go its details view which requires data from /api/teams/{id} if it isn't already present in the Store.
If Actions handle all the XHRs, the View would do something like TeamActions.get([id]) which does TeamDAO.get([id]). To be able to return this call immediately(since we have it cached) the DAO would have to do caching but also maintain the relation between collections/items. This logic, by design, is already present in Stores.
Here come the questions:
Do you duplicate this logic in DAOs and Stores?
Do you make DAO's aware of Stores and they can ask the Store if they already have some data and just return a 302 saying, you're good you have the latest data.
How do you handle validation that involves XHR APIs? Something simple like duplicate Team names.
Views directly hit DAOs and do something like TeamDAO.validateName([name]) which returns a promise or do you do you create an Action? If you create an Action through which Store does Valid/Invalid flow back to the View considering its mostly transient data?
How do you handle Routing? I looked through react-router and I'm not sure I like it. I don't necessarily think forcing a react-ish JSX way of providing route mappings/configs are needed at all. Also, apparently, it employs a RouteDispatcher of its own, which ondoes the single dispatcher rule.
The solution I prefer came from some blog posts/SO answers where you have a the route mappings are stored in the RouteStore.
RouteStore also maintains CURRENT_VIEW. The react AppContainer component is registered with RouteStore and replaces its child views with the CURRENT_VIEW on change. Current Views inform the AppContainer when they're fully loaded and AppContainer fires RouteActions.pending/success/failure, possibly with some context, to inform other components of reaching a stable state, show/hide busy/loading indications.
Something that I have not been able to design cleanly was if you were to design routing similar to Gmail, how would you do it? Some observations of Gmail that I'm a big fan of:
URLs don't change until the page is ready to load. It stays on the current URL while its 'Loading' and moves to the new one once the loading has finished. This makes it so that...
On failure, you don't lose you current page at all. So if you're on compose, and the 'Send' fails, you don't lose your mail (i.e. you don't lose your current stable view/state). (they don't do this because auto saving is le pwn, but you get the idea) You have the option of copy/pasting the mail somewhere for safe keeping till you can send again.
Some references:
https://github.com/gaearon/flux-react-router-example
http://ianobermiller.com/blog/2014/09/15/react-and-flux-interview/
https://github.com/facebook/flux
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
Stores are responsible for maintaining data state through Immutable.Record and maintaining cache through a global Immutable.OrderedMap referencing Record instance via ids.
Stores directly call WebAPIUtils for read operations and trigger actions for write operations.
Relationship between 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)
Stores only expose getters methods such as get(id), getAll(), etc.
APIUTILS
I use SuperAgent for ajax calls. Each request is wrapped in Promise
I use a map of read request Promise indexed by the hash of url + params
I trigger action through ActionCreators such as fooReceived or fooError when Promise is resolved or rejected.
fooError action should certainly contains payloads with validation errors returned by the server.
COMPONENTS
The controller-view component listen for changes in store(s).
All my components, other than controller-view component, are 'pure', so I use ImmutableRenderMixin to only re-render what it's really needed (meaning that if you print Perf.printWasted time, it should be very low, few ms.
Since Relay and GraphQL are not yet open sourced, I enforce to keep my component props as explicit as possible via propsType.
Parent component should only passes down the necessary props. If my parent component holds an object such as 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
IMPLEMENTATION
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:
State 1:
User hits the page /foos/ listing the Foos via the FooListcontroller-view component
FooListcontroller-view component calls FooStore.getAll()
_foos map is empty in FooStore so FooStore performs a request via FooAPI.getAll()
The 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 action
FooStore receive this action, updates its internal state, and emits change.
State 2:
FooList controller-view component receive change event and update its state to get the list from the FooStore
this.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).
We're passing needed props to the FooListItem component.
By calling 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 BarStore
BarStore does not have this Bar record in its _bars cache, so it triggers a request through BarAPI to retrieve it.
The same happens for all Foo in this.sate.fooList of FooList controller-view component
The page now looks something like this:
++++++++++++++++++++++++
+ +
+ 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...
State 3:
The FooList controller-view component responds to the BarStore change by updating its state.
The render method is called
The foo.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).
The page now looks like this :
++++++++++++++++++++++++
+ +
+ 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 :).
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 :/.
Related
Having a Backbone model shared between several views is a rather common situation. Nevertheless, let's say this model is a UserModel. It handles several methods, allowing a user to register or login for instance.
And when the user is logged, fetch is called to get the user's data. Therefor the model can't fetch itself with this.fetch() in its initialize method.
Where shoud it be fetched from? How?
This is our simple UserModel:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
login(email, password, rememberMe, callback) {
…
},
signup(email, password, firstname, lastname, callback) {
…
}
});
Now let's say it's shared by both:
HomeView & CartView
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
app.CartView = Backbone.View.extend({
template: app.tpl.cart,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
Now if I instanciate HomeView, userModel will be fetched. But if at a later point, I instanciate CartView, this same model will be fetched again. And that makes a useless http request.
Basically, the model could fetch istself after a succesfull call to its login method, but user can arrive on a page or reload his browser being already logged in. Furthermore, a user can land on any page, there's no way to say he's going to HomeView prior to CartView.
There are two options I see. Either UserModel smartly handles multiple fetch calls like so:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
fetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
That way, I'm able to call this.model.fetch() as many times as needed, being stateless in views.
Or, I can handle that on a view layer:
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
// fetch model if empty
if (_.isEmpty(this.model.changed)) this.fetch();
// render directly if already populated
else this.render();
// render on model sync
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
If needed, Backbone's model.changed doc reference & Underscore's _.isEmpty's.
Which way is cleaner? Is there any other approach I might have missed?
Personal preference would not be to override fetch but to instead implement a wrapper function, like customFetch
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
The code example you provided would end up in a loop (this.fetch calling itself...), so my personal preference is to just wrap the core backbone functionality in another function.
I would even go so far as to have my own custom Model that is extended by all the models I use. Eg:
const MyModel = Backbone.Model.extend({
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
return (this.fetchTime && hourAgo > this.fetchTime);
},
customFetch() {
this.fetch();
},
});
Then UserModel would override customFetch and look like this:
const UserModel = MyModel.extend({
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
});
Might not be the best way to do it. For me personally it would be the easy way for it to read and then extend later. I would imagine this customFetch would be used in some/all models so it could be amended as appropriate.
In a example about pagination, different number is passed to the route in terms of how many results should be displayed.
// lib/router.js
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
},
data: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return {
posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
};
}
});
I am wondering if it's necessary to still give the Posts.find() arguments in the data attribute. It seems kinda redundant here because the subscribe in waitOn already limit the dataset that i am getting back from the server. I know that data is used to provide data context for different parts of my templates. However, providing the same arguments here to data Posts.find() just seems redundant. I have tried just using Posts.find() without the argument and it worked.
Also, I am wondering what is a way to access all the data in router? I am accessing the Posts collections outside the individual route and I thought that I would be able to access all the data in Posts collections. However, it returned with a count 0.
// lib/router.js
var totalPostsCount = Posts.find().count()
console.log('total count?')
console.log(totalPostsCount) // output 0
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
// console.log("is find limited to subscriptions too??");
// console.log(Posts.find().count())
return Posts.find({}, this.findOptions());
},
data: function() {
// console.log('is data limited to this?')
// console.log(Posts.find().count())
var hasMore = this.posts().count() === this.postsLimit();
var adjustOrNot = this.posts()
if (!hasMore){
var nextPath = this.route.path({postsLimit: this.posts().count()});
}else{
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
}
return {
posts: this.posts(),
ready:this.postsSub.ready(),
nextPath: hasMore ? nextPath : null
};
}
});
//...
Router.route('/:postsLimit?', {
name: 'postsList'
});
Edit 1: code for getting all posts:
// server
Meteor.publish('allPosts',function(){
return Posts.find({});
})
// lib/router.js
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
console.log('not ready yet');
}
}
This only outputs 'not ready yet' in the console and it's not changing even when the page finish loading.
Thanks a lot for the help.
Edit 2: possible solutions
I tried wrapping the reactive source ready() inside a computation such as Tracker.autorun() and now it worked.
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
Tracker.autorun(function(){
var status = handle.ready();
if (status){
console.log(Posts.find().count())
}else{
console.log('not ready yet')
}
})
}
Yes, you should pass limit and other query options to the front-end query too, regardless of your subscription. This is because if you were to have multiple subscriptions to the same collection active at the same time (as is often done in larger apps) the subscribed records end up all in the same Minimongo collection in the front-end. In this case, if you had omitted the query parameters, you would end up with unpredictable results. See more in this excellent explanation. Note also that while iron:router supports doing subscriptions in the route handlers, it's encouraged to handle them in the template lifecycle methods.
For your second question, the reason the Posts collection appears empty is that the code in the top level of the file is run immediately as the file is loaded. At this point, the collection subscription isn't loaded yet, so the front-end collection is empty. You need to check for subscription readiness in some reactive context, such as a template helper or Tracker.autorun, to ensure that the data is loaded:
var handle = Meteor.subscribe('posts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
// This logs 0 (unless there are other subscriptions to this collection)
console.log(Posts.find().count());
}
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
My application has three things that are "global": a user location with lat and lng values, if the user is logged in or not, and the activity that they have chosen (think of it like a category).
What I'd like to happen is for these values to be used across the application. Each screen will use some set of this date, so loading it per page isn't the right answer. The information also will not change unless the user creates an event: different location, activity, or signs in/out.
How should I set this up?
My thinking is that there should be an initial load upon startup, in my main.js file I load each and then app.start like so:
siteService.js
define([...], function (...) {
return {
init: function () {
return $.when(
activityService.getStoredActivity(),
locationService.getStoredLocation(),
userService.getUsername()
);
}
}
});
main.js
define([..., 'modules/siteService'], function (..., site) {
...
site.init().then(function () {
app.start().then(function () {
...
app.setRoot('viewmodels/shell', 'entrance');
});
});
});
This does make sure that the data is loaded. From there in each of my services I store the information into local storage so I don't have to keep firing off AJAX calls. If local storage isn't there it will.
The problem comes later when I need this data in other pages. I'm ending up calling my site.init().then() within each page's activate method. Which is pretty sloppy.
shell.js
self.activate = function () {
...
siteService.init().then(function (activity, location, username) {
self.activity(activity);
self.location(location);
setUsername(username);
});
return router.activate();
};
and then again in:
welcome.js
this.activate = function () {
site.init().then(function(activity, location) {
loc = location;
act = activity;
load();
});
});
I'd like to set the values initially, each page initially loading from those values, and then use the pub/sub of Durandal to react to any changes after the fact.
I was thinking of solely using the pub/sub, but then felt that I would run into chicken and the egg issues when the data is loaded in relation to the page loads (IE If my AJAX loaded first, and my page wasn't loaded, the event fires, but the page never gets the update).
Is it possible to setup something like a static class in javascript to pull the data initially and then share it throughout the viewmodels? Or, is there a way to guarantee that the value will be there when I initially load a page, like a app.on('value', { initialize: true })?
When a require.js module returns an object, that object is a singleton, so you can have something like
settings.js
define(function() {
return {
location: {
lat: 0,
long: 0
},
loggedin: false,
activity: ""
}
});
Then any time you name settings as a dependency, you'll always get a reference to the same object and you can get/set its fields as you wish.
Changed siteService.js to:
define([...], function (...) {
var module = function () { };
module.prototype.activity = {};
module.prototype.location = {};
module.prototype.username = {};
module.prototype.init = function () {
return $.when(
activityService.getStoredActivity(),
locationService.getStoredLocation(),
userService.getUsername()
).then(function (activity, location, username) {
module.prototype.activity = activity;
module.prototype.location = location;
module.prototype.username = username;
});
};
return module;
});
main.js to
define([..., 'modules/siteService'], function(..., Site) {
site = new Site();
site.init().then(function () {
app.start().then(function () {
...
app.setRoot('viewmodels/shell', 'entrance');
});
});
and shell.js and welcome.js now just use:
var site = new Site();
self.activity(site.activity);
self.location(site.location);
setUsername(site.username);
and
var site = new Site();
loc = site.location;
act = site.activity;
The short version : I have a Backbone model which has as one of its attributes, a second Backbone model. A function in the first model changes the state of the second model, but my view, which is listening for changes to the first model, does not seem to be picking up the state of the second model, despite any amount of logging that suggests otherwise (I've been logging this at various points to confirm scope, etc.). How can I fix this?
The long version : I have a Backbone model Course, which represents an academic course, and a model NameList, which represents the list of students enrolled in a Course. A Course "has-a" NameList. A NameList is backed by a single text file on the server.
I want to have a function in the Course model called importNameList which creates a new NameList model and causes that NameList model to go and fetch its data from the backend. Since my view, CourseView is listening for changes to the Course model, and a Course model has-a NameList, it seems like this should update the view accordingly. My problem is that it doesn't.
What I want to do
var course = new Course();
var courseView = new CourseView({model : course});
courseView.model.importNameList('students.txt'); // Will cause view to re-render
What I have to do
var course = new Course(); // same
var courseView = new CourseView({model : course}); // same
courseView.model.importNameList('students.txt'); // same
courseView.render(); // Argh, manually calling this is ugly.
Here is my code with logging statements. The models extend Backbone.DeepModel only because I thought it might solve my problem, but it didn't.
Console output
[console] var course = new Course();
[console] var courseView = new CourseView({model : course});
[console] course.importNameList('students.txt');
Course >>> importNameList
NameList >>> initialize()
NameList >>> fetch()
GET http://localhost/files/students.txt 200 OK 16ms
CourseView >>> render() // Render starts before fetch completes
CourseView <<< render() // Render finishes before fetch completes
Course <<< importNameList
NameList <<< fetch()
[console] courseView.render();
CourseView >>> render()
alice
bob
charlie
dan
erica
fred
george
CourseView <<< render()
Course.js
var Course = Backbone.DeepModel.extend({
defaults : {
nameList : new NameList()
},
initialize: function(options) {
if (options && options.nameList) {
this.set({nameList : options.nameList});
}
},
importNameList : function(fileName) {
console.log("Course >>> importNameList");
var nameList = new NameList({fileName : fileName});
this.set({nameList : nameList});
console.log("Course <<< importNameList");
}
});
NameList.js
var NameList = Backbone.DeepModel.extend({
defaults : {
fileName : 'new.txt',
names : []
},
initialize: function(options) {
console.log("NameList >>> initialize()");
var model = this;
if (options && options.fileName) {
model.set({fileName : options.fileName});
}
model.fetch();
},
fetch : function() {
console.log("NameList >>> fetch()");
var model = this;
$.ajax({
url : '/files/' + model.get('fileName'),
success : function(response) {
model.set({names : response.split('\n')});
console.log("NameList <<< fetch()");
}
});
}
});
CourseView.js
var CourseView = Backbone.View.extend({
initialize : function(options) {
var view = this;
if (options && options.model) {
view.model = options.model;
} else {
view.model = new Course();
}
view.model.on('change', function() {
view.render();
});
},
render : function() {
console.log("CourseView >>> render()");
var names = this.model.get('nameList').get('names');
for (var i = 0; i < names.length; i++) {
console.log(names[i]);
}
console.log("CourseView <<< render()");
return this;
}
});
The quick answer is to use jQuery a deferred object. You can find more on using deferreds with backbone.js in this article.
I would add more specific info but I'm not really clear why you're overriding fetch, which seems like a recipe for disaster. I would stick to backbone's original fetch method and leave it out of NameList's initializer, call it instead from Course.js and use the deferred returned from it to guarantee that the steps after are run once the fetch is completed.
UPDATE:
Here's a sketch of one way you could do this. First take the model.fetch(); line out of the initialize method of NameList, and call it instead in importNameList, using its return value (a deferred object) as the return value of importNameList:
var nameList = new NameList({fileName : fileName});
this.set({nameList : nameList});
return nameList.fetch()
Now that we are returning a deferred from importNameList, we can use it to guarantee that the fetch is complete before rendering:
this.deferred = courseView.model.importNameList('students.txt');
this.deferred.done(function() { courseView.render() });
I think that should do what you want, although I haven't actually tested it.
I ended up doing the following.
CourseView.js:
// No changes.
Within Course.js:
importNameList : function(name) {
var model = this;
var nameList = new NameList({fileName : fileName});
// Set the attribute silently (without triggering a change event)
model.set({nameList : nameList}, {silent: true});
nameList.fetch({
success : function() {
model.change(); // Manually fire a change event when fetch completes.
}
});
}
Within NameList.js:
fetch : function(options) {
var model = this;
$.ajax({
url : '/files/' + model.get('fileName'),
success : function(response) {
model.set({lines : response.split('\n')});
// Make the "success" callback.
options.success();
}
});
}