diff --git a/packages/react-navigation/examples/NavigationPlayground/App.test.js b/packages/react-navigation/examples/NavigationPlayground/App.test.js
index fc6f975e..15668c90 100644
--- a/packages/react-navigation/examples/NavigationPlayground/App.test.js
+++ b/packages/react-navigation/examples/NavigationPlayground/App.test.js
@@ -5,5 +5,6 @@ import renderer from 'react-test-renderer';
it('renders without crashing', () => {
const rendered = renderer.create().toJSON();
- expect(rendered).toBeTruthy();
+ // Will be null because the playground uses state persistence which happens asyncronously
+ expect(rendered).toEqual(null);
});
diff --git a/packages/react-navigation/examples/NavigationPlayground/js/App.js b/packages/react-navigation/examples/NavigationPlayground/js/App.js
index 2326bbe5..6c56bb91 100644
--- a/packages/react-navigation/examples/NavigationPlayground/js/App.js
+++ b/packages/react-navigation/examples/NavigationPlayground/js/App.js
@@ -307,7 +307,7 @@ const AppNavigator = createStackNavigator(
}
);
-export default () => ;
+export default () => ;
const styles = StyleSheet.create({
item: {
diff --git a/packages/react-navigation/flow/react-navigation.js b/packages/react-navigation/flow/react-navigation.js
index 7e0154c3..d385a0fe 100644
--- a/packages/react-navigation/flow/react-navigation.js
+++ b/packages/react-navigation/flow/react-navigation.js
@@ -538,6 +538,8 @@ declare module 'react-navigation' {
NavigationAction
) => void,
navigation?: NavigationScreenProp,
+ persistenceKey?: ?string,
+ renderLoadingExperimental?: React$ComponentType<{}>,
screenProps?: *,
navigationOptions?: O,
}>;
diff --git a/packages/react-navigation/src/createNavigationContainer.js b/packages/react-navigation/src/createNavigationContainer.js
index 59fb591a..baa0dea0 100644
--- a/packages/react-navigation/src/createNavigationContainer.js
+++ b/packages/react-navigation/src/createNavigationContainer.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { Linking } from 'react-native';
import withLifecyclePolyfill from 'react-lifecycles-compat';
+import { Linking, AsyncStorage } from 'react-native';
import { BackHandler } from './PlatformHelpers';
import NavigationActions from './NavigationActions';
@@ -32,6 +32,13 @@ function validateProps(props) {
}
}
+// 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.
@@ -73,16 +80,44 @@ export default function createNavigationContainer(Component) {
}
this.state = {
- nav: this._isStateful()
- ? Component.router.getStateForAction(this._initialAction)
- : null,
+ nav:
+ this._isStateful() && !props.persistenceKey
+ ? Component.router.getStateForAction(this._initialAction)
+ : null,
};
}
+ _renderLoading() {
+ return this.props.renderLoadingExperimental
+ ? this.props.renderLoadingExperimental()
+ : null;
+ }
+
_isStateful() {
return isStateful(this.props);
}
+ _validateProps(props) {
+ if (this._isStateful()) {
+ return;
+ }
+
+ const { navigation, screenProps, ...containerProps } = props;
+
+ 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.'
+ );
+ }
+ }
+
_urlToPathAndParams(url) {
const params = {};
const delimiter = this.props.uriPrefix || '://';
@@ -145,7 +180,7 @@ export default function createNavigationContainer(Component) {
}
}
- componentDidMount() {
+ async componentDidMount() {
this._isMounted = true;
if (!this._isStateful()) {
return;
@@ -153,18 +188,71 @@ export default function createNavigationContainer(Component) {
Linking.addEventListener('url', this._handleOpenURL);
- Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
+ const { persistenceKey } = this.props;
+ const startupStateJSON =
+ persistenceKey && (await AsyncStorage.getItem(persistenceKey));
+ let startupState = null;
+ try {
+ startupState = startupStateJSON && JSON.parse(startupStateJSON);
+ _reactNavigationIsHydratingState = true;
+ } catch (e) {}
- this._actionEventSubscribers.forEach(subscriber =>
- subscriber({
- type: 'action',
- action: this._initialAction,
- state: this.state.nav,
- lastState: null,
- })
- );
+ let action = this._initialAction;
+ if (!startupState) {
+ !!process.env.REACT_NAV_LOGGING &&
+ console.log('Init new Navigation State');
+ startupState = Component.router.getStateForAction(action);
+ }
+
+ const url = await Linking.getInitialURL();
+ const parsedUrl = url && this._urlToPathAndParams(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:', url);
+ action = urlAction;
+ startupState = Component.router.getStateForAction(
+ urlAction,
+ startupState
+ );
+ }
+ }
+ this.setState({ nav: startupState }, () => {
+ _reactNavigationIsHydratingState = false;
+ this._actionEventSubscribers.forEach(subscriber =>
+ subscriber({
+ type: 'action',
+ action,
+ state: this.state.nav,
+ lastState: null,
+ })
+ );
+ });
}
+ componentDidCatch(e, errorInfo) {
+ 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());
+ }
+ }
+
+ _persistNavigationState = async nav => {
+ const { persistenceKey } = this.props;
+ if (!persistenceKey) {
+ return;
+ }
+ await AsyncStorage.setItem(persistenceKey, JSON.stringify(nav));
+ };
+
componentWillUnmount() {
this._isMounted = false;
Linking.removeEventListener('url', this._handleOpenURL);
@@ -174,8 +262,8 @@ export default function createNavigationContainer(Component) {
// Per-tick temporary storage for state.nav
dispatch = action => {
- if (!this._isStateful()) {
- return false;
+ if (this.props.navigation) {
+ return this.props.navigation.dispatch(action);
}
this._nav = this._nav || this.state.nav;
const oldNav = this._nav;
@@ -197,6 +285,7 @@ export default function createNavigationContainer(Component) {
this.setState({ nav }, () => {
this._onNavigationStateChange(oldNav, nav, action);
dispatchActionEvents();
+ this._persistNavigationState(nav);
});
return true;
} else {
@@ -209,7 +298,9 @@ export default function createNavigationContainer(Component) {
let navigation = this.props.navigation;
if (this._isStateful()) {
const nav = this.state.nav;
- invariant(nav, 'should be set in constructor if stateful');
+ if (!nav) {
+ return this._renderLoading();
+ }
if (!this._navigation || this._navigation.state !== nav) {
this._navigation = addNavigationHelpers({
dispatch: this.dispatch,