mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-10 17:23:42 +08:00
State persistence (#3716)
Adds a new state persistence mechanisms to all of the navigators via createNavigationContainer
There are two new props that you can provide to a navigation container: `storageKey` and `renderLoading`.
`storageKey` is the string key to be used with AsyncStorage while saving and persisting navigation state. To disable persistence, set to null.
`renderLoading` allows you to render something while the navigator re-hydrates its state and loads its initial URL. By default this returns null, but Expo users will want to render a <AppLoading /> component for smooth app launches
There is also functionality in this PR to observe errors that come from re-hydrating state, and gracefully recover by dispatching an init action.
Also this revises the init action to *reset* the navigation state, rather than preserve the previous state.
This commit is contained in:
@@ -5,5 +5,6 @@ import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const rendered = renderer.create(<App />).toJSON();
|
||||
expect(rendered).toBeTruthy();
|
||||
// Will be null because the playground uses state persistence which happens asyncronously
|
||||
expect(rendered).toEqual(null);
|
||||
});
|
||||
|
||||
@@ -307,7 +307,7 @@ const AppNavigator = createStackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
export default () => <AppNavigator />;
|
||||
export default () => <AppNavigator persistenceKey="NavState" />;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -538,6 +538,8 @@ declare module 'react-navigation' {
|
||||
NavigationAction
|
||||
) => void,
|
||||
navigation?: NavigationScreenProp<S>,
|
||||
persistenceKey?: ?string,
|
||||
renderLoadingExperimental?: React$ComponentType<{}>,
|
||||
screenProps?: *,
|
||||
navigationOptions?: O,
|
||||
}>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user