Skip to content

[added] Router.AsyncState mixin #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 27, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions modules/helpers/resolveAsyncState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var Promise = require('es6-promise').Promise;

/**
* Resolves all values in asyncState and calls the setState
* function with new state as they resolve. Returns a promise
* that resolves after all values are resolved.
*/
function resolveAsyncState(asyncState, setState) {
if (asyncState == null)
return Promise.resolve();

var keys = Object.keys(asyncState);

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

module.exports = resolveAsyncState;
1 change: 1 addition & 0 deletions modules/main.js
Original file line number Diff line number Diff line change
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
113 changes: 113 additions & 0 deletions modules/mixins/AsyncState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably note here that state will be automatically set to the resolved value, calling setState is just for additional state related to the async operation.

*
* Much like the familiar getInitialState method, getInitialAsyncState
* should return a hash of key/value pairs to use in the component's
* state. The difference is that the values may be promises. As these
* values resolve, the component's state is updated. You should only
* ever need to use the setState function for doing things like
* streaming data and/or updating progress.
*
* 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
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, maybe we should bring in sinon or something else to fake timers (later, this is fine).

});
});

});
1 change: 1 addition & 0 deletions specs/main.js
Original file line number Diff line number Diff line change
@@ -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