Skip to content

Commit 97dbf2d

Browse files
committedSep 25, 2014
[added] transition.wait(promise)
[removed] Ability to return a promise from willTransition* hooks This commit makes async transitions entirely optional. Users opt-in to using asynchronous transitions using transition.wait(promise). Fixes #309 Fixes #300 Fixes #295
1 parent cc9f145 commit 97dbf2d

File tree

3 files changed

+116
-70
lines changed

3 files changed

+116
-70
lines changed
 

‎modules/components/Routes.js

+105-70
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ var React = require('react');
22
var warning = require('react/lib/warning');
33
var copyProperties = require('react/lib/copyProperties');
44
var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM;
5-
var Promise = require('when/lib/Promise');
65
var LocationActions = require('../actions/LocationActions');
76
var Route = require('../components/Route');
87
var ActiveDelegate = require('../mixins/ActiveDelegate');
98
var PathListener = require('../mixins/PathListener');
109
var RouteStore = require('../stores/RouteStore');
1110
var Path = require('../utils/Path');
11+
var Promise = require('../utils/Promise');
1212
var Redirect = require('../utils/Redirect');
1313
var Transition = require('../utils/Transition');
1414

@@ -40,9 +40,7 @@ function defaultAbortedTransitionHandler(transition) {
4040
* error so that it isn't silently swallowed.
4141
*/
4242
function defaultTransitionErrorHandler(error) {
43-
setTimeout(function () { // Use setTimeout to break the promise chain.
44-
throw error; // This error probably originated in a transition hook.
45-
});
43+
throw error; // This error probably originated in a transition hook.
4644
}
4745

4846
/**
@@ -119,47 +117,50 @@ var Routes = React.createClass({
119117
return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute, this.props.notFoundRoute);
120118
},
121119

120+
updatePath: function (path) {
121+
var self = this;
122+
123+
this.dispatch(path, function (error, transition) {
124+
if (error) {
125+
self.props.onTransitionError(error);
126+
} else if (transition.isAborted) {
127+
self.props.onAbortedTransition(transition);
128+
} else {
129+
self.emitChange();
130+
maybeUpdateScroll(self);
131+
}
132+
});
133+
},
134+
122135
/**
123-
* Performs a transition to the given path and returns a promise for the
124-
* Transition object that was used.
136+
* Performs a transition to the given path and calls callback(error, transition)
137+
* with the Transition object when the transition is finished and the component's
138+
* state has been updated accordingly.
125139
*
126-
* In order to do this, the router first determines which routes are involved
127-
* in the transition beginning with the current route, up the route tree to
128-
* the first parent route that is shared with the destination route, and back
129-
* down the tree to the destination route. The willTransitionFrom static
130-
* method is invoked on all route handlers we're transitioning away from, in
131-
* reverse nesting order. Likewise, the willTransitionTo static method
132-
* is invoked on all route handlers we're transitioning to.
140+
* In a transition, the router first determines which routes are involved by
141+
* beginning with the current route, up the route tree to the first parent route
142+
* that is shared with the destination route, and back down the tree to the
143+
* destination route. The willTransitionFrom hook is invoked on all route handlers
144+
* we're transitioning away from, in reverse nesting order. Likewise, the
145+
* willTransitionTo hook is invoked on all route handlers we're transitioning to.
133146
*
134-
* Both willTransitionFrom and willTransitionTo hooks may either abort or
135-
* redirect the transition. If they need to resolve asynchronously, they may
136-
* return a promise.
147+
* Both willTransitionFrom and willTransitionTo hooks may either abort or redirect
148+
* the transition. To resolve asynchronously, they may use transition.wait(promise).
137149
*
138150
* Note: This function does not update the URL in a browser's location bar.
139-
* If you want to keep the URL in sync with transitions, use Router.transitionTo,
140-
* Router.replaceWith, or Router.goBack instead.
141151
*/
142-
updatePath: function (path) {
143-
var routes = this;
152+
dispatch: function (path, callback) {
144153
var transition = new Transition(path);
154+
var self = this;
155+
156+
computeNextState(this, transition, function (error, nextState) {
157+
if (error || nextState == null)
158+
return callback(error, transition);
145159

146-
return runTransitionHooks(routes, transition)
147-
.then(function (newState) {
148-
if (transition.isAborted)
149-
routes.props.onAbortedTransition(transition);
150-
151-
if (newState == null)
152-
return transition;
153-
154-
return new Promise(function (resolve) {
155-
routes.setState(newState, function () {
156-
routes.emitChange();
157-
maybeUpdateScroll(routes);
158-
resolve(transition);
159-
});
160-
});
161-
})
162-
.then(undefined, this.props.onTransitionError);
160+
self.setState(nextState, function () {
161+
callback(null, transition);
162+
});
163+
});
163164
},
164165

165166
render: function () {
@@ -248,14 +249,13 @@ function updateMatchComponents(matches, refs) {
248249
}
249250

250251
/**
251-
* Runs all transition hooks that are required to get from the current state
252-
* to the state specified by the given transition and updates the current state
253-
* if they all pass successfully. Returns a promise that resolves to the new
254-
* state if it needs to be updated, or undefined if not.
252+
* Computes the next state for the given <Routes> component and calls
253+
* callback(error, nextState) when finished. Also runs all transition
254+
* hooks along the way.
255255
*/
256-
function runTransitionHooks(routes, transition) {
256+
function computeNextState(routes, transition, callback) {
257257
if (routes.state.path === transition.path)
258-
return Promise.resolve(); // Nothing to do!
258+
return callback(); // Nothing to do!
259259

260260
var currentMatches = routes.state.matches;
261261
var nextMatches = routes.match(transition.path);
@@ -287,26 +287,26 @@ function runTransitionHooks(routes, transition) {
287287

288288
var query = Path.extractQuery(transition.path) || {};
289289

290-
return runTransitionFromHooks(fromMatches, transition).then(function () {
291-
if (transition.isAborted)
292-
return; // No need to continue.
290+
runTransitionFromHooks(fromMatches, transition, function (error) {
291+
if (error || transition.isAborted)
292+
return callback(error);
293293

294-
return runTransitionToHooks(toMatches, transition, query).then(function () {
295-
if (transition.isAborted)
296-
return; // No need to continue.
294+
runTransitionToHooks(toMatches, transition, query, function (error) {
295+
if (error || transition.isAborted)
296+
return callback(error);
297297

298298
var rootMatch = getRootMatch(nextMatches);
299299
var params = (rootMatch && rootMatch.params) || {};
300300

301-
return {
301+
callback(null, {
302302
path: transition.path,
303303
matches: nextMatches,
304304
activeParams: params,
305305
activeQuery: query,
306306
activeRoutes: nextMatches.map(function (match) {
307307
return match.route;
308308
})
309-
};
309+
});
310310
});
311311
});
312312
}
@@ -315,41 +315,76 @@ function runTransitionHooks(routes, transition) {
315315
* Calls the willTransitionFrom hook of all handlers in the given matches
316316
* serially in reverse with the transition object and the current instance of
317317
* the route's handler, so that the deepest nested handlers are called first.
318-
* Returns a promise that resolves after the last handler.
318+
* Calls callback(error) when finished.
319319
*/
320-
function runTransitionFromHooks(matches, transition) {
321-
var promise = Promise.resolve();
322-
323-
reversedArray(matches).forEach(function (match) {
324-
promise = promise.then(function () {
320+
function runTransitionFromHooks(matches, transition, callback) {
321+
var hooks = reversedArray(matches).map(function (match) {
322+
return function () {
325323
var handler = match.route.props.handler;
326324

327325
if (!transition.isAborted && handler.willTransitionFrom)
328326
return handler.willTransitionFrom(transition, match.component);
329-
});
327+
328+
var promise = transition.promise;
329+
delete transition.promise;
330+
331+
return promise;
332+
};
330333
});
331334

332-
return promise;
335+
runHooks(hooks, callback);
333336
}
334337

335338
/**
336-
* Calls the willTransitionTo hook of all handlers in the given matches serially
337-
* with the transition object and any params that apply to that handler. Returns
338-
* a promise that resolves after the last handler.
339+
* Calls the willTransitionTo hook of all handlers in the given matches
340+
* serially with the transition object and any params that apply to that
341+
* handler. Calls callback(error) when finished.
339342
*/
340-
function runTransitionToHooks(matches, transition, query) {
341-
var promise = Promise.resolve();
342-
343-
matches.forEach(function (match) {
344-
promise = promise.then(function () {
343+
function runTransitionToHooks(matches, transition, query, callback) {
344+
var hooks = matches.map(function (match) {
345+
return function () {
345346
var handler = match.route.props.handler;
346347

347348
if (!transition.isAborted && handler.willTransitionTo)
348-
return handler.willTransitionTo(transition, match.params, query);
349-
});
349+
handler.willTransitionTo(transition, match.params, query);
350+
351+
var promise = transition.promise;
352+
delete transition.promise;
353+
354+
return promise;
355+
};
350356
});
351357

352-
return promise;
358+
runHooks(hooks, callback);
359+
}
360+
361+
/**
362+
* Runs all hook functions serially and calls callback(error) when finished.
363+
* A hook may return a promise if it needs to execute asynchronously.
364+
*/
365+
function runHooks(hooks, callback) {
366+
try {
367+
var promise = hooks.reduce(function (promise, hook) {
368+
// The first hook to use transition.wait makes the rest
369+
// of the transition async from that point forward.
370+
return promise ? promise.then(hook) : hook();
371+
}, null);
372+
} catch (error) {
373+
return callback(error); // Sync error.
374+
}
375+
376+
if (promise) {
377+
// Use setTimeout to break the promise chain.
378+
promise.then(function () {
379+
setTimeout(callback);
380+
}, function (error) {
381+
setTimeout(function () {
382+
callback(error);
383+
});
384+
});
385+
} else {
386+
callback();
387+
}
353388
}
354389

355390
/**

‎modules/utils/Promise.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var Promise = require('when/lib/Promise');
2+
3+
// TODO: Use process.env.NODE_ENV check + envify to enable
4+
// when's promise monitor here when in dev.
5+
6+
module.exports = Promise;

‎modules/utils/Transition.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var mixInto = require('react/lib/mixInto');
2+
var Promise = require('./Promise');
23
var Redirect = require('./Redirect');
34
var replaceWith = require('../actions/LocationActions').replaceWith;
45

@@ -25,6 +26,10 @@ mixInto(Transition, {
2526
this.abort(new Redirect(to, params, query));
2627
},
2728

29+
wait: function (value) {
30+
this.promise = Promise.resolve(value);
31+
},
32+
2833
retry: function () {
2934
replaceWith(this.path);
3035
}

0 commit comments

Comments
 (0)
Please sign in to comment.