This repository has been archived by the owner on Feb 25, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathcreateAppContainer.js
438 lines (389 loc) · 13.3 KB
/
createAppContainer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
import React from 'react';
import { Linking, Platform, BackHandler } from 'react-native';
import {
NavigationActions,
ThemeProvider,
pathUtils,
getNavigation,
NavigationProvider,
} from '@react-navigation/core';
import invariant from './utils/invariant';
import docsUrl from './utils/docsUrl';
const { urlToPathAndParams } = pathUtils;
function isStateful(props) {
return !props.navigation;
}
function validateProps(props) {
if (props.persistenceKey) {
console.warn(
'You passed persistenceKey prop to a navigator. ' +
'The persistenceKey prop was replaced by a more flexible persistence mechanism, ' +
'please see the navigation state persistence docs for more information. ' +
'Passing the persistenceKey prop is a no-op.'
);
}
if (isStateful(props)) {
return;
}
/* eslint-disable no-unused-vars */
const {
navigation,
screenProps,
persistNavigationState,
loadNavigationState,
theme,
...containerProps
} = props;
/* eslint-enable no-unused-vars */
const keys = Object.keys(containerProps);
if (keys.length !== 0) {
throw new Error(
'This navigator has both navigation and container props, so it is ' +
`unclear if it should own its own state. Remove props: "${keys.join(
', '
)}" ` +
'if the navigator should get its state from the navigation prop. If the ' +
'navigator should maintain its own state, do not pass a navigation prop.'
);
}
invariant(
(persistNavigationState === undefined &&
loadNavigationState === undefined) ||
(typeof persistNavigationState === 'function' &&
typeof loadNavigationState === 'function'),
'both persistNavigationState and loadNavigationState must either be undefined, or be functions'
);
}
// Track the number of stateful container instances. Warn if >0 and not using the
// detached prop to explicitly acknowledge the behavior. We should deprecated implicit
// stateful navigation containers in a future release and require a provider style pattern
// instead in order to eliminate confusion entirely.
let _statefulContainerCount = 0;
export function _TESTING_ONLY_reset_container_count() {
_statefulContainerCount = 0;
}
// We keep a global flag to catch errors during the state persistence hydrating scenario.
// The innermost navigator who catches the error will dispatch a new init action.
let _reactNavigationIsHydratingState = false;
// Unfortunate to use global state here, but it seems necessesary for the time
// being. There seems to be some problems with cascading componentDidCatch
// handlers. Ideally the inner non-stateful navigator catches the error and
// re-throws it, to be caught by the top-level stateful navigator.
/**
* Create an HOC that injects the navigation and manages the navigation state
* in case it's not passed from above.
* This allows to use e.g. the StackNavigator and TabNavigator as root-level
* components.
*/
export default function createNavigationContainer(Component) {
class NavigationContainer extends React.Component {
subs = null;
static router = Component.router;
static navigationOptions = null;
static defaultProps = {
theme: 'light',
};
static getDerivedStateFromProps(nextProps) {
validateProps(nextProps);
return null;
}
_actionEventSubscribers = new Set();
constructor(props) {
super(props);
validateProps(props);
this._initialAction = NavigationActions.init();
if (
this._isStateful() &&
BackHandler &&
typeof BackHandler.addEventListener === 'function'
) {
this.subs = BackHandler.addEventListener('hardwareBackPress', () => {
if (!this._isMounted) {
this.subs && this.subs.remove();
} else {
// dispatch returns true if the action results in a state change,
// and false otherwise. This maps well to what BackHandler expects
// from a callback -- true if handled, false if not handled
return this.dispatch(NavigationActions.back());
}
});
}
this.state = {
nav:
this._isStateful() && !props.loadNavigationState
? Component.router.getStateForAction(this._initialAction)
: null,
};
}
_renderLoading() {
return this.props.renderLoadingExperimental
? this.props.renderLoadingExperimental()
: null;
}
_isStateful() {
return isStateful(this.props);
}
_handleOpenURL = ({ url }) => {
const { enableURLHandling, uriPrefix } = this.props;
if (enableURLHandling === false) {
return;
}
const parsedUrl = urlToPathAndParams(url, uriPrefix);
if (parsedUrl) {
const { path, params } = parsedUrl;
const action = Component.router.getActionForPathAndParams(path, params);
if (action) {
this.dispatch(action);
}
}
};
_onNavigationStateChange(prevNav, nav, action) {
if (
typeof this.props.onNavigationStateChange === 'undefined' &&
this._isStateful() &&
!!process.env.REACT_NAV_LOGGING
) {
if (console.group) {
console.group('Navigation Dispatch: ');
console.log('Action: ', action);
console.log('New State: ', nav);
console.log('Last State: ', prevNav);
console.groupEnd();
} else {
console.log('Navigation Dispatch: ', {
action,
newState: nav,
lastState: prevNav,
});
}
return;
}
if (typeof this.props.onNavigationStateChange === 'function') {
this.props.onNavigationStateChange(prevNav, nav, action);
}
}
componentDidUpdate() {
// Clear cached _navState every tick
if (this._navState === this.state.nav) {
this._navState = null;
}
}
async componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
}
if (__DEV__ && !this.props.detached) {
if (_statefulContainerCount > 0) {
// Temporarily only show this on iOS due to this issue:
// https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829
if (Platform.OS === 'ios') {
console.warn(
`You should only render one navigator explicitly in your app, and other navigators should be rendered by including them in that navigator. Full details at: ${docsUrl(
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
)}`
);
}
}
}
_statefulContainerCount++;
Linking.addEventListener('url', this._handleOpenURL);
// Pull out anything that can impact state
let parsedUrl = null;
let userProvidedStartupState = null;
if (this.props.enableURLHandling !== false) {
({
parsedUrl,
userProvidedStartupState,
} = await this.getStartupParams());
}
// Initialize state. This must be done *after* any async code
// so we don't end up with a different value for this.state.nav
// due to changes while async function was resolving
let action = this._initialAction;
let startupState = this.state.nav;
if (!startupState && !userProvidedStartupState) {
!!process.env.REACT_NAV_LOGGING &&
console.log('Init new Navigation State');
startupState = Component.router.getStateForAction(action);
}
// Pull user-provided persisted state
if (userProvidedStartupState) {
startupState = userProvidedStartupState;
_reactNavigationIsHydratingState = true;
}
// Pull state out of URL
if (parsedUrl) {
const { path, params } = parsedUrl;
const urlAction = Component.router.getActionForPathAndParams(
path,
params
);
if (urlAction) {
!!process.env.REACT_NAV_LOGGING &&
console.log(
'Applying Navigation Action for Initial URL:',
parsedUrl
);
action = urlAction;
startupState = Component.router.getStateForAction(
urlAction,
startupState
);
}
}
const dispatchActions = () =>
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action,
state: this.state.nav,
lastState: null,
})
);
if (startupState === this.state.nav) {
dispatchActions();
return;
}
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ nav: startupState }, () => {
_reactNavigationIsHydratingState = false;
dispatchActions();
});
}
async getStartupParams() {
const { uriPrefix, loadNavigationState } = this.props;
let url, loadedNavState;
try {
[url, loadedNavState] = await Promise.all([
Linking.getInitialURL(),
loadNavigationState && loadNavigationState(),
]);
} catch (err) {
// ignore
}
return {
parsedUrl: url && urlToPathAndParams(url, uriPrefix),
userProvidedStartupState: loadedNavState,
};
}
componentDidCatch(e) {
if (_reactNavigationIsHydratingState) {
_reactNavigationIsHydratingState = false;
console.warn(
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state...'
);
this.dispatch(NavigationActions.init());
} else {
throw e;
}
}
_persistNavigationState = async nav => {
const { persistNavigationState } = this.props;
if (persistNavigationState) {
try {
await persistNavigationState(nav);
} catch (err) {
console.warn(
'Uncaught exception while calling persistNavigationState()! You should handle exceptions thrown from persistNavigationState(), ignoring them may result in undefined behavior.'
);
}
}
};
componentWillUnmount() {
this._isMounted = false;
Linking.removeEventListener('url', this._handleOpenURL);
this.subs && this.subs.remove();
if (this._isStateful()) {
_statefulContainerCount--;
}
}
// Per-tick temporary storage for state.nav
dispatch = action => {
if (this.props.navigation) {
return this.props.navigation.dispatch(action);
}
// navState will have the most up-to-date value, because setState sometimes behaves asyncronously
this._navState = this._navState || this.state.nav;
const lastNavState = this._navState;
invariant(lastNavState, 'should be set in constructor if stateful');
const reducedState = Component.router.getStateForAction(
action,
lastNavState
);
const navState = reducedState === null ? lastNavState : reducedState;
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action,
state: navState,
lastState: lastNavState,
})
);
};
if (reducedState === null) {
// The router will return null when action has been handled and the state hasn't changed.
// dispatch returns true when something has been handled.
dispatchActionEvents();
return true;
}
if (navState !== lastNavState) {
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
this._navState = navState;
this.setState({ nav: navState }, () => {
this._onNavigationStateChange(lastNavState, navState, action);
dispatchActionEvents();
this._persistNavigationState(navState);
});
return true;
}
dispatchActionEvents();
return false;
};
_getScreenProps = () => this.props.screenProps;
_getTheme = () => {
if (this.props.theme === 'light' || this.props.theme === 'dark') {
return this.props.theme;
} else if (this.props.theme === 'no-preference') {
return 'light';
} else {
console.warn(
`Invalid theme provided: ${
this.props.theme
}. Only 'light' and 'dark' are supported. Falling back to 'light'`
);
return 'light';
}
};
render() {
let navigation = this.props.navigation;
if (this._isStateful()) {
const navState = this.state.nav;
if (!navState) {
return this._renderLoading();
}
if (!this._navigation || this._navigation.state !== navState) {
this._navigation = getNavigation(
Component.router,
navState,
this.dispatch,
this._actionEventSubscribers,
this._getScreenProps,
() => this._navigation
);
}
navigation = this._navigation;
}
invariant(navigation, 'failed to get navigation');
return (
<ThemeProvider value={this._getTheme()}>
<NavigationProvider value={navigation}>
<Component {...this.props} navigation={navigation} />
</NavigationProvider>
</ThemeProvider>
);
}
}
return NavigationContainer;
}