Skip to content

Commit

Permalink
[added] Router.AsyncState mixin
Browse files Browse the repository at this point in the history
Helps with #57
  • Loading branch information
mjackson committed Jul 27, 2014
1 parent b0aa491 commit 5fea685
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 0 deletions.
24 changes: 24 additions & 0 deletions modules/helpers/resolveAsyncState.js
@@ -0,0 +1,24 @@
var Promise = require('es6-promise').Promise;

/**
* Resolves all values in the given stateDescription object
* and calls the setState function with new state as they resolve.
*/
function resolveAsyncState(stateDescription, setState) {
if (stateDescription == null)
return Promise.resolve({});

var keys = Object.keys(stateDescription);

return Promise.all(
keys.map(function (key) {
return Promise.resolve(stateDescription[key]).then(function (value) {
var newState = {};
newState[key] = value;
setState(newState);
});
})
);
}

module.exports = resolveAsyncState;
1 change: 1 addition & 0 deletions modules/main.js
Expand Up @@ -7,6 +7,7 @@ exports.replaceWith = require('./helpers/replaceWith');
exports.transitionTo = require('./helpers/transitionTo');

exports.ActiveState = require('./mixins/ActiveState');
exports.AsyncState = require('./mixins/AsyncState');

// Backwards compat with 0.1. We should
// remove this when we ship 1.0.
Expand Down
106 changes: 106 additions & 0 deletions modules/mixins/AsyncState.js
@@ -0,0 +1,106 @@
var React = require('react');
var resolveAsyncState = require('../helpers/resolveAsyncState');

/**
* A mixin for route handler component classes that fetch at least
* part of their state asynchronously. Classes that use it should
* declare a static `getInitialAsyncState` method that fetches state
* for a component after it mounts. This function is given three
* arguments: 1) the current route params, 2) the current query and
* 3) a function that can be used to set state as it is received.
*
* Example:
*
* var User = React.createClass({
*
* statics: {
*
* getInitialAsyncState: function (params, query, setState) {
* // If you don't need to do anything async, just update
* // the state immediately and you're done.
* setState({
* user: UserStore.getUserByID(params.userID)
* });
*
* // Or, ignore the setState argument entirely and return a
* // hash with keys named after the state variables you want
* // to set. The values may be immediate values or promises.
* return {
* user: getUserByID(params.userID) // may be a promise
* };
*
* // Or, stream your data!
* var buffer = '';
*
* return {
*
* // Same as above, the stream state variable is set to the
* // value returned by this promise when it resolves.
* stream: getStreamingData(params.userID, function (chunk) {
* buffer += chunk;
*
* // Notify of progress.
* setState({
* streamBuffer: buffer
* });
* })
*
* };
* }
*
* },
*
* getInitialState: function () {
* return {
* user: null, // Receives a value when getUserByID resolves.
* stream: null, // Receives a value when getStreamingData resolves.
* streamBuffer: '' // Used to track data as it loads.
* };
* },
*
* render: function () {
* if (!this.state.user)
* return <LoadingUser/>;
*
* return (
* <div>
* <p>Welcome {this.state.user.name}!</p>
* <p>So far, you've received {this.state.streamBuffer.length} data!</p>
* </div>
* );
* }
*
* });
*
* When testing, use the `initialAsyncState` prop to simulate asynchronous
* data fetching. When this prop is present, no attempt is made to retrieve
* additional state via `getInitialAsyncState`.
*/
var AsyncState = {

propTypes: {
initialAsyncState: React.PropTypes.object
},

getInitialState: function () {
return this.props.initialAsyncState || null;
},

updateAsyncState: function (state) {
if (this.isMounted())
this.setState(state);
},

componentDidMount: function () {
if (this.props.initialAsyncState || !this.constructor.getInitialAsyncState)
return;

resolveAsyncState(
this.constructor.getInitialAsyncState(this.props.params, this.props.query, this.updateAsyncState),
this.updateAsyncState
);
}

};

module.exports = AsyncState;
47 changes: 47 additions & 0 deletions specs/AsyncState.spec.js
@@ -0,0 +1,47 @@
require('./helper');
var Promise = require('es6-promise').Promise;
var AsyncState = require('../modules/mixins/AsyncState');

describe('AsyncState', function () {


describe('a component that fetches part of its state asynchronously', function () {
it('resolves all state variables correctly', function (done) {
var User = React.createClass({
mixins: [ AsyncState ],
statics: {
getInitialAsyncState: function (params, query, setState) {
setState({
immediateValue: 'immediate'
});

setTimeout(function () {
setState({
delayedValue: 'delayed'
});
});

return {
promisedValue: Promise.resolve('promised')
};
}
},
render: function () {
return null;
}
});

var user = TestUtils.renderIntoDocument(
User()
);

setTimeout(function () {
expect(user.state.immediateValue).toEqual('immediate');
expect(user.state.delayedValue).toEqual('delayed');
expect(user.state.promisedValue).toEqual('promised');
done();
}, 20);
});
});

});
1 change: 1 addition & 0 deletions specs/main.js
@@ -1,6 +1,7 @@
// TODO: this is for webkpack-karma to only create one build instead of a build
// for every spec file, there must be some sort of config but I can't find it ...
require('./ActiveStore.spec.js');
require('./AsyncState.spec.js');
require('./Path.spec.js');
require('./Route.spec.js');
require('./RouteStore.spec.js');
Expand Down

0 comments on commit 5fea685

Please sign in to comment.