Skip to content

Commit 8f22edd

Browse files
committed
Merge pull request #125 from rackt/async-state
[added] Router.AsyncState mixin
2 parents 2a53f7c + 0f908f1 commit 8f22edd

File tree

5 files changed

+187
-0
lines changed

5 files changed

+187
-0
lines changed

modules/helpers/resolveAsyncState.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
var Promise = require('es6-promise').Promise;
2+
3+
/**
4+
* Resolves all values in asyncState and calls the setState
5+
* function with new state as they resolve. Returns a promise
6+
* that resolves after all values are resolved.
7+
*/
8+
function resolveAsyncState(asyncState, setState) {
9+
if (asyncState == null)
10+
return Promise.resolve();
11+
12+
var keys = Object.keys(asyncState);
13+
14+
return Promise.all(
15+
keys.map(function (key) {
16+
return Promise.resolve(asyncState[key]).then(function (value) {
17+
var newState = {};
18+
newState[key] = value;
19+
setState(newState);
20+
});
21+
})
22+
);
23+
}
24+
25+
module.exports = resolveAsyncState;

modules/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ exports.replaceWith = require('./helpers/replaceWith');
77
exports.transitionTo = require('./helpers/transitionTo');
88

99
exports.ActiveState = require('./mixins/ActiveState');
10+
exports.AsyncState = require('./mixins/AsyncState');
1011

1112
// Backwards compat with 0.1. We should
1213
// remove this when we ship 1.0.

modules/mixins/AsyncState.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
var React = require('react');
2+
var resolveAsyncState = require('../helpers/resolveAsyncState');
3+
4+
/**
5+
* A mixin for route handler component classes that fetch at least
6+
* part of their state asynchronously. Classes that use it should
7+
* declare a static `getInitialAsyncState` method that fetches state
8+
* for a component after it mounts. This function is given three
9+
* arguments: 1) the current route params, 2) the current query and
10+
* 3) a function that can be used to set state as it is received.
11+
*
12+
* Much like the familiar getInitialState method, getInitialAsyncState
13+
* should return a hash of key/value pairs to use in the component's
14+
* state. The difference is that the values may be promises. As these
15+
* values resolve, the component's state is updated. You should only
16+
* ever need to use the setState function for doing things like
17+
* streaming data and/or updating progress.
18+
*
19+
* Example:
20+
*
21+
* var User = React.createClass({
22+
*
23+
* statics: {
24+
*
25+
* getInitialAsyncState: function (params, query, setState) {
26+
* // If you don't need to do anything async, just update
27+
* // the state immediately and you're done.
28+
* setState({
29+
* user: UserStore.getUserByID(params.userID)
30+
* });
31+
*
32+
* // Or, ignore the setState argument entirely and return a
33+
* // hash with keys named after the state variables you want
34+
* // to set. The values may be immediate values or promises.
35+
* return {
36+
* user: getUserByID(params.userID) // may be a promise
37+
* };
38+
*
39+
* // Or, stream your data!
40+
* var buffer = '';
41+
*
42+
* return {
43+
*
44+
* // Same as above, the stream state variable is set to the
45+
* // value returned by this promise when it resolves.
46+
* stream: getStreamingData(params.userID, function (chunk) {
47+
* buffer += chunk;
48+
*
49+
* // Notify of progress.
50+
* setState({
51+
* streamBuffer: buffer
52+
* });
53+
* })
54+
*
55+
* };
56+
* }
57+
*
58+
* },
59+
*
60+
* getInitialState: function () {
61+
* return {
62+
* user: null, // Receives a value when getUserByID resolves.
63+
* stream: null, // Receives a value when getStreamingData resolves.
64+
* streamBuffer: '' // Used to track data as it loads.
65+
* };
66+
* },
67+
*
68+
* render: function () {
69+
* if (!this.state.user)
70+
* return <LoadingUser/>;
71+
*
72+
* return (
73+
* <div>
74+
* <p>Welcome {this.state.user.name}!</p>
75+
* <p>So far, you've received {this.state.streamBuffer.length} data!</p>
76+
* </div>
77+
* );
78+
* }
79+
*
80+
* });
81+
*
82+
* When testing, use the `initialAsyncState` prop to simulate asynchronous
83+
* data fetching. When this prop is present, no attempt is made to retrieve
84+
* additional state via `getInitialAsyncState`.
85+
*/
86+
var AsyncState = {
87+
88+
propTypes: {
89+
initialAsyncState: React.PropTypes.object
90+
},
91+
92+
getInitialState: function () {
93+
return this.props.initialAsyncState || null;
94+
},
95+
96+
updateAsyncState: function (state) {
97+
if (this.isMounted())
98+
this.setState(state);
99+
},
100+
101+
componentDidMount: function () {
102+
if (this.props.initialAsyncState || !this.constructor.getInitialAsyncState)
103+
return;
104+
105+
resolveAsyncState(
106+
this.constructor.getInitialAsyncState(this.props.params, this.props.query, this.updateAsyncState),
107+
this.updateAsyncState
108+
);
109+
}
110+
111+
};
112+
113+
module.exports = AsyncState;

specs/AsyncState.spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require('./helper');
2+
var Promise = require('es6-promise').Promise;
3+
var AsyncState = require('../modules/mixins/AsyncState');
4+
5+
describe('AsyncState', function () {
6+
7+
8+
describe('a component that fetches part of its state asynchronously', function () {
9+
it('resolves all state variables correctly', function (done) {
10+
var User = React.createClass({
11+
mixins: [ AsyncState ],
12+
statics: {
13+
getInitialAsyncState: function (params, query, setState) {
14+
setState({
15+
immediateValue: 'immediate'
16+
});
17+
18+
setTimeout(function () {
19+
setState({
20+
delayedValue: 'delayed'
21+
});
22+
});
23+
24+
return {
25+
promisedValue: Promise.resolve('promised')
26+
};
27+
}
28+
},
29+
render: function () {
30+
return null;
31+
}
32+
});
33+
34+
var user = TestUtils.renderIntoDocument(
35+
User()
36+
);
37+
38+
setTimeout(function () {
39+
expect(user.state.immediateValue).toEqual('immediate');
40+
expect(user.state.delayedValue).toEqual('delayed');
41+
expect(user.state.promisedValue).toEqual('promised');
42+
done();
43+
}, 20);
44+
});
45+
});
46+
47+
});

specs/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// TODO: this is for webkpack-karma to only create one build instead of a build
22
// for every spec file, there must be some sort of config but I can't find it ...
33
require('./ActiveStore.spec.js');
4+
require('./AsyncState.spec.js');
45
require('./Path.spec.js');
56
require('./Route.spec.js');
67
require('./RouteStore.spec.js');

0 commit comments

Comments
 (0)