@@ -4,13 +4,19 @@ import invariant from 'invariant';
4
4
import { loopAsync } from './AsyncUtils' ;
5
5
import { createRoutes } from './RouteUtils' ;
6
6
import { pathnameIsActive , queryIsActive } from './ActiveUtils' ;
7
- import { getState , getTransitionHooks , createTransitionHook , getComponents , getRouteParams } from './RoutingUtils' ;
7
+ import { getState , getTransitionHooks , getComponents , createTransitionHook , getRouteParams , runTransition } from './RoutingUtils' ;
8
8
import { routes , component , components , history , location } from './PropTypes' ;
9
9
import Location from './Location' ;
10
10
11
- var { any , array , func, object, instanceOf } = React . PropTypes ;
11
+ var { arrayOf , func, object } = React . PropTypes ;
12
12
13
- var ContextMixin = {
13
+ var ServerTransitionDelegate = {
14
+ getState,
15
+ getTransitionHooks,
16
+ getComponents
17
+ } ;
18
+
19
+ var RoutingContextMixin = {
14
20
15
21
childContextTypes : {
16
22
router : object . isRequired
@@ -72,32 +78,89 @@ var ContextMixin = {
72
78
73
79
} ;
74
80
81
+ var TransitionDelegateMixin = {
82
+
83
+ /**
84
+ * Adds a transition hook that runs before all route hooks in a
85
+ * transition. The signature is the same as route transition hooks.
86
+ */
87
+ addTransitionHook ( hook ) {
88
+ if ( ! this . transitionHooks )
89
+ this . transitionHooks = [ ] ;
90
+
91
+ this . transitionHooks . push ( hook ) ;
92
+ } ,
93
+
94
+ /**
95
+ * Removes the given transition hook.
96
+ */
97
+ removeTransitionHook ( hook ) {
98
+ if ( this . transitionHooks )
99
+ this . transitionHooks = this . transitionHooks . filter ( h => h !== hook ) ;
100
+ } ,
101
+
102
+ getState ( routes , location , callback ) {
103
+ var { branch, params } = this . props ;
104
+
105
+ if ( branch && params ) {
106
+ callback ( null , { branch, params } ) ;
107
+ } else {
108
+ getState ( routes , location , callback ) ;
109
+ }
110
+ } ,
111
+
112
+ getTransitionHooks ( prevState , nextState ) {
113
+ var hooks = [ ] ;
114
+
115
+ // Run component hooks before route hooks.
116
+ if ( this . transitionHooks )
117
+ hooks . push . apply ( hooks , this . transitionHooks . map ( hook => createTransitionHook ( hook , this ) ) ) ;
118
+
119
+ hooks . push . apply ( hooks , getTransitionHooks ( prevState , nextState ) ) ;
120
+
121
+ return hooks ;
122
+ } ,
123
+
124
+ getComponents ( nextState , callback ) {
125
+ var { components } = this . props ;
126
+
127
+ if ( components ) {
128
+ callback ( null , components ) ;
129
+ } else {
130
+ getComponents ( nextState , callback ) ;
131
+ }
132
+ }
133
+
134
+ } ;
135
+
75
136
export var Router = React . createClass ( {
76
137
77
- mixins : [ ContextMixin ] ,
138
+ mixins : [ RoutingContextMixin , TransitionDelegateMixin ] ,
78
139
79
140
statics : {
80
141
81
- match ( routes , history , callback ) {
82
- // TODO: Mimic what we're doing in _updateState, but statically
83
- // so we can get the right props for doing server-side rendering.
142
+ match ( routes , location , callback ) {
143
+ runTransition ( null , routes , location , ServerTransitionDelegate , callback ) ;
84
144
}
85
145
86
146
} ,
87
147
88
148
propTypes : {
89
- history : history . isRequired ,
90
- children : routes ,
91
- routes, // Alias for children
92
149
createElement : func . isRequired ,
93
150
onError : func . isRequired ,
94
151
onUpdate : func ,
95
152
96
- // For server-side rendering
97
- location : any ,
153
+ // Client-side
154
+ history,
155
+ routes,
156
+ // Routes may also be given as children (JSX)
157
+ children : routes ,
158
+
159
+ // Server-side
160
+ location,
98
161
branch : routes ,
99
162
params : object ,
100
- components
163
+ components : arrayOf ( components )
101
164
} ,
102
165
103
166
getDefaultProps ( ) {
@@ -126,133 +189,44 @@ export var Router = React.createClass({
126
189
'A <Router> needs a valid Location'
127
190
) ;
128
191
129
- this . nextLocation = location ;
130
192
this . setState ( { isTransitioning : true } ) ;
131
193
132
- this . _getState ( this . routes , location , ( error , state ) => {
133
- if ( error || this . nextLocation !== location ) {
134
- this . _finishTransition ( error ) ;
194
+ runTransition ( this . state , this . routes , location , this , ( error , transition , state ) => {
195
+ this . setState ( { isTransitioning : false } ) ;
196
+
197
+ if ( error ) {
198
+ this . handleError ( error ) ;
199
+ } else if ( transition . isCancelled ) {
200
+ if ( transition . redirectInfo ) {
201
+ var { pathname, query, state } = transition . redirectInfo ;
202
+ this . replaceWith ( pathname , query , state ) ;
203
+ } else {
204
+ invariant (
205
+ this . state . location ,
206
+ 'You may not abort the initial transition'
207
+ ) ;
208
+
209
+ // TODO: Do something with transition.abortReason ?
210
+
211
+ // The best we can do here is goBack so the location state reverts
212
+ // to what it was. However, we also set a flag so that we know not
213
+ // to run through _updateState again since state did not change.
214
+ this . _ignoreNextHistoryChange = true ;
215
+ this . goBack ( ) ;
216
+ }
135
217
} else if ( state == null ) {
136
218
warning ( false , 'Location "%s" did not match any routes' , location . pathname ) ;
137
- this . _finishTransition ( ) ;
138
219
} else {
139
- state . location = location ;
140
-
141
- this . _runTransitionHooks ( state , ( error ) => {
142
- if ( error || this . nextLocation !== location ) {
143
- this . _finishTransition ( error ) ;
144
- } else {
145
- this . _getComponents ( state , ( error , components ) => {
146
- if ( error || this . nextLocation !== location ) {
147
- this . _finishTransition ( error ) ;
148
- } else {
149
- state . components = components ;
150
-
151
- this . _finishTransition ( null , state ) ;
152
- }
153
- } ) ;
154
- }
155
- } ) ;
220
+ this . setState ( state , this . props . onUpdate ) ;
221
+ this . _alreadyUpdated = true ;
156
222
}
157
223
} ) ;
158
224
} ,
159
225
160
- _finishTransition ( error , state ) {
161
- this . setState ( { isTransitioning : false } ) ;
162
- this . nextLocation = null ;
163
-
164
- if ( error ) {
165
- this . handleError ( error ) ;
166
- } else if ( state ) {
167
- this . setState ( state , this . props . onUpdate ) ;
168
- this . _alreadyUpdated = true ;
169
- }
170
- } ,
171
-
172
- _getState ( routes , location , callback ) {
173
- var { branch, params } = this . props ;
174
-
175
- if ( branch && params && query ) {
176
- callback ( null , { branch, params } ) ;
177
- } else {
178
- getState ( routes , location , callback ) ;
179
- }
180
- } ,
181
-
182
- _runTransitionHooks ( nextState , callback ) {
183
- // Run component hooks before route hooks.
184
- var hooks = this . transitionHooks . map ( hook => createTransitionHook ( hook , this ) ) ;
185
-
186
- hooks . push . apply (
187
- hooks ,
188
- getTransitionHooks ( this . state , nextState )
189
- ) ;
190
-
191
- var nextLocation = this . nextLocation ;
192
-
193
- loopAsync ( hooks . length , ( index , next , done ) => {
194
- var hook = hooks [ index ] ;
195
-
196
- hooks [ index ] . call ( this , nextState , this , ( error ) => {
197
- if ( error || this . nextLocation !== nextLocation ) {
198
- done . call ( this , error ) ; // No need to continue.
199
- } else {
200
- next . call ( this ) ;
201
- }
202
- } ) ;
203
- } , callback ) ;
204
- } ,
205
-
206
- _getComponents ( nextState , callback ) {
207
- if ( this . props . components ) {
208
- callback ( null , this . props . components ) ;
209
- } else {
210
- getComponents ( nextState , callback ) ;
211
- }
212
- } ,
213
-
214
226
_createElement ( component , props ) {
215
227
return typeof component === 'function' ? this . props . createElement ( component , props ) : null ;
216
228
} ,
217
229
218
- /**
219
- * Adds a transition hook that runs before all route hooks in a
220
- * transition. The signature is the same as route transition hooks.
221
- */
222
- addTransitionHook ( hook ) {
223
- this . transitionHooks . push ( hook ) ;
224
- } ,
225
-
226
- /**
227
- * Removes the given transition hook.
228
- */
229
- removeTransitionHook ( hook ) {
230
- this . transitionHooks = this . transitionHooks . filter ( h => h !== hook ) ;
231
- } ,
232
-
233
- /**
234
- * Cancels the current transition, preventing any subsequent transition
235
- * hooks from running and restoring the previous location.
236
- */
237
- cancelTransition ( ) {
238
- invariant (
239
- this . state . location ,
240
- 'Router#cancelTransition: You may not cancel the initial transition'
241
- ) ;
242
-
243
- if ( this . nextLocation ) {
244
- this . nextLocation = null ;
245
-
246
- // The best we can do here is goBack so the location state reverts
247
- // to what it was. However, we also set a flag so that we know not
248
- // to run through _updateState again.
249
- this . _ignoreNextHistoryChange = true ;
250
- this . goBack ( ) ;
251
- } else {
252
- warning ( false , 'Router#cancelTransition: Router is not transitioning' ) ;
253
- }
254
- } ,
255
-
256
230
handleHistoryChange ( ) {
257
231
if ( this . _ignoreNextHistoryChange ) {
258
232
this . _ignoreNextHistoryChange = false ;
@@ -264,23 +238,34 @@ export var Router = React.createClass({
264
238
componentWillMount ( ) {
265
239
var { history, routes, children } = this . props ;
266
240
267
- invariant (
268
- routes || children ,
269
- 'A <Router> needs some routes'
270
- ) ;
241
+ if ( history ) {
242
+ invariant (
243
+ routes || children ,
244
+ 'A client-side <Router> needs some routes. Try using <Router routes> or ' +
245
+ 'passing your routes as nested <Route> children'
246
+ ) ;
247
+
248
+ this . routes = createRoutes ( routes || children ) ;
249
+
250
+ if ( typeof history . setup === 'function' )
251
+ history . setup ( ) ;
271
252
272
- this . routes = createRoutes ( routes || children ) ;
273
- this . transitionHooks = [ ] ;
274
- this . nextLocation = null ;
253
+ // We need to listen first in case we redirect immediately.
254
+ if ( history . addChangeListener )
255
+ history . addChangeListener ( this . handleHistoryChange ) ;
275
256
276
- if ( typeof history . setup === 'function' )
277
- history . setup ( ) ;
257
+ this . _updateState ( history . location ) ;
258
+ } else {
259
+ var { location, branch, params, components } = this . props ;
278
260
279
- // We need to listen first in case we redirect immediately.
280
- if ( history . addChangeListener )
281
- history . addChangeListener ( this . handleHistoryChange ) ;
261
+ invariant (
262
+ location && branch && params && components ,
263
+ 'A server-side <Router> needs location, branch, params, and components ' +
264
+ 'props. Try using Router.match to get all the props you need'
265
+ ) ;
282
266
283
- this . _updateState ( history . location ) ;
267
+ this . setState ( { location, branch, params, components } ) ;
268
+ }
284
269
} ,
285
270
286
271
componentDidMount ( ) {
@@ -298,23 +283,25 @@ export var Router = React.createClass({
298
283
'<Router history> may not be changed'
299
284
) ;
300
285
301
- var currentRoutes = this . props . routes || this . props . children ;
302
- var nextRoutes = nextProps . routes || nextProps . children ;
286
+ if ( nextProps . history ) {
287
+ var currentRoutes = this . props . routes || this . props . children ;
288
+ var nextRoutes = nextProps . routes || nextProps . children ;
303
289
304
- if ( currentRoutes !== nextRoutes ) {
305
- this . routes = createRoutes ( nextRoutes ) ;
290
+ if ( currentRoutes !== nextRoutes ) {
291
+ this . routes = createRoutes ( nextRoutes ) ;
306
292
307
- // Call this here because _updateState
308
- // uses this.routes to determine state.
309
- if ( nextProps . history . location )
310
- this . _updateState ( nextProps . history . location ) ;
293
+ // Call this here because _updateState
294
+ // uses this.routes to determine state.
295
+ if ( nextProps . history . location )
296
+ this . _updateState ( nextProps . history . location ) ;
297
+ }
311
298
}
312
299
} ,
313
300
314
301
componentWillUnmount ( ) {
315
302
var { history } = this . props ;
316
303
317
- if ( history . removeChangeListener )
304
+ if ( history && history . removeChangeListener )
318
305
history . removeChangeListener ( this . handleHistoryChange ) ;
319
306
} ,
320
307
0 commit comments