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,