mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 04:45:19 +08:00
refactor: add transaction support (#12)
This commit is contained in:
committed by
Michal Osadnik
parent
38aa8e447b
commit
cb89debb94
@@ -18,8 +18,9 @@ const MISSING_CONTEXT_ERROR =
|
|||||||
export const NavigationStateContext = React.createContext<{
|
export const NavigationStateContext = React.createContext<{
|
||||||
state?: NavigationState | PartialState;
|
state?: NavigationState | PartialState;
|
||||||
getState: () => NavigationState | PartialState | undefined;
|
getState: () => NavigationState | PartialState | undefined;
|
||||||
setState: (state: NavigationState | undefined) => void;
|
setState: (state: NavigationState | undefined, dangerously?: boolean) => void;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
performTransaction: (action: () => void) => void;
|
||||||
}>({
|
}>({
|
||||||
get getState(): any {
|
get getState(): any {
|
||||||
throw new Error(MISSING_CONTEXT_ERROR);
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
@@ -27,6 +28,9 @@ export const NavigationStateContext = React.createContext<{
|
|||||||
get setState(): any {
|
get setState(): any {
|
||||||
throw new Error(MISSING_CONTEXT_ERROR);
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
},
|
},
|
||||||
|
get performTransaction(): any {
|
||||||
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class NavigationContainer extends React.Component<Props, State> {
|
export default class NavigationContainer extends React.Component<Props, State> {
|
||||||
@@ -42,10 +46,39 @@ export default class NavigationContainer extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNavigationState = () => this.state.navigationState;
|
private navigationState:
|
||||||
|
| NavigationState
|
||||||
|
| PartialState
|
||||||
|
| undefined
|
||||||
|
| null = null;
|
||||||
|
|
||||||
private setNavigationState = (navigationState: NavigationState | undefined) =>
|
private performTransaction = (action: () => void) => {
|
||||||
this.setState({ navigationState });
|
this.setState(
|
||||||
|
state => {
|
||||||
|
this.navigationState = state.navigationState;
|
||||||
|
action();
|
||||||
|
return { navigationState: this.navigationState };
|
||||||
|
},
|
||||||
|
() => (this.navigationState = null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private getNavigationState = () =>
|
||||||
|
this.navigationState || this.state.navigationState;
|
||||||
|
|
||||||
|
private setNavigationState = (
|
||||||
|
navigationState: NavigationState | undefined,
|
||||||
|
dangerously = false
|
||||||
|
) => {
|
||||||
|
if (this.navigationState === null && !dangerously) {
|
||||||
|
throw new Error('setState need to be wrapped in a performTransaction');
|
||||||
|
}
|
||||||
|
if (dangerously) {
|
||||||
|
this.setState({ navigationState });
|
||||||
|
} else {
|
||||||
|
this.navigationState = navigationState;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
@@ -54,6 +87,7 @@ export default class NavigationContainer extends React.Component<Props, State> {
|
|||||||
state: this.state.navigationState,
|
state: this.state.navigationState,
|
||||||
getState: this.getNavigationState,
|
getState: this.getNavigationState,
|
||||||
setState: this.setNavigationState,
|
setState: this.setNavigationState,
|
||||||
|
performTransaction: this.performTransaction,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EnsureSingleNavigator>{this.props.children}</EnsureSingleNavigator>
|
<EnsureSingleNavigator>{this.props.children}</EnsureSingleNavigator>
|
||||||
|
|||||||
@@ -19,24 +19,27 @@ type Props = {
|
|||||||
|
|
||||||
export default function SceneView(props: Props) {
|
export default function SceneView(props: Props) {
|
||||||
const { screen, route, navigation: helpers, getState, setState } = props;
|
const { screen, route, navigation: helpers, getState, setState } = props;
|
||||||
|
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
const navigation = React.useMemo(
|
const navigation = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...helpers,
|
...helpers,
|
||||||
setParams: (params: object) => {
|
setParams: (params: object) => {
|
||||||
const state = getState();
|
performTransaction(() => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
routes: state.routes.map(r =>
|
routes: state.routes.map(r =>
|
||||||
r.key === route.key
|
r.key === route.key
|
||||||
? { ...r, params: { ...r.params, ...params } }
|
? { ...r, params: { ...r.params, ...params } }
|
||||||
: r
|
: r
|
||||||
),
|
),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[getState, helpers, route.key, setState]
|
[getState, helpers, performTransaction, route.key, setState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCurrentState = React.useCallback(() => {
|
const getCurrentState = React.useCallback(() => {
|
||||||
@@ -65,9 +68,16 @@ export default function SceneView(props: Props) {
|
|||||||
state: route.state,
|
state: route.state,
|
||||||
getState: getCurrentState,
|
getState: getCurrentState,
|
||||||
setState: setCurrentState,
|
setState: setCurrentState,
|
||||||
|
performTransaction,
|
||||||
key: route.key,
|
key: route.key,
|
||||||
}),
|
}),
|
||||||
[getCurrentState, route.key, route.state, setCurrentState]
|
[
|
||||||
|
getCurrentState,
|
||||||
|
performTransaction,
|
||||||
|
route.key,
|
||||||
|
route.state,
|
||||||
|
setCurrentState,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { render } from 'react-native-testing-library';
|
import { render } from 'react-native-testing-library';
|
||||||
import { NavigationStateContext } from '../NavigationContainer';
|
import NavigationContainer, {
|
||||||
|
NavigationStateContext,
|
||||||
|
} from '../NavigationContainer';
|
||||||
|
|
||||||
it('throws when getState is accessed without a container', () => {
|
it('throws when getState is accessed without a container', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
@@ -39,3 +41,46 @@ it('throws when setState is accessed without a container', () => {
|
|||||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws when performTransaction is accessed without a container', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const Test = () => {
|
||||||
|
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
|
// eslint-disable-next-line babel/no-unused-expressions
|
||||||
|
performTransaction;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = <Test />;
|
||||||
|
|
||||||
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
|
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when setState is called outside performTransaction', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const Test = () => {
|
||||||
|
const { setState } = React.useContext(NavigationStateContext);
|
||||||
|
React.useEffect(() => {
|
||||||
|
setState(undefined);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<NavigationContainer>
|
||||||
|
<Test />
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
|
'setState need to be wrapped in a performTransaction'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default function useNavigationBuilder(
|
|||||||
getState: getCurrentState,
|
getState: getCurrentState,
|
||||||
setState,
|
setState,
|
||||||
key,
|
key,
|
||||||
|
performTransaction,
|
||||||
} = React.useContext(NavigationStateContext);
|
} = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
let state = router.getRehydratedState({
|
let state = router.getRehydratedState({
|
||||||
@@ -93,7 +94,7 @@ export default function useNavigationBuilder(
|
|||||||
// If the state needs to be updated, we'll schedule an update with React
|
// If the state needs to be updated, we'll schedule an update with React
|
||||||
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
|
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
|
||||||
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
|
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
|
||||||
setState(nextState);
|
setState(nextState, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = nextState;
|
state = nextState;
|
||||||
@@ -102,7 +103,9 @@ export default function useNavigationBuilder(
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
// We need to clean up state for this navigator on unmount
|
// We need to clean up state for this navigator on unmount
|
||||||
getCurrentState() !== undefined && setState(undefined);
|
performTransaction(
|
||||||
|
() => getCurrentState() !== undefined && setState(undefined)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NavigationState,
|
NavigationState,
|
||||||
ActionCreators,
|
ActionCreators,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { NavigationStateContext } from './NavigationContainer';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
|
onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
|
||||||
@@ -25,15 +26,19 @@ export default function useNavigationHelpers({
|
|||||||
NavigationBuilderContext
|
NavigationBuilderContext
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
return React.useMemo((): NavigationHelpers => {
|
return React.useMemo((): NavigationHelpers => {
|
||||||
const dispatch = (
|
const dispatch = (
|
||||||
action: NavigationAction | ((state: NavigationState) => NavigationState)
|
action: NavigationAction | ((state: NavigationState) => NavigationState)
|
||||||
) => {
|
) => {
|
||||||
if (typeof action === 'function') {
|
performTransaction(() => {
|
||||||
setState(action(getState()));
|
if (typeof action === 'function') {
|
||||||
} else {
|
setState(action(getState()));
|
||||||
onAction(action);
|
} else {
|
||||||
}
|
onAction(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -54,5 +59,12 @@ export default function useNavigationHelpers({
|
|||||||
),
|
),
|
||||||
dispatch,
|
dispatch,
|
||||||
};
|
};
|
||||||
}, [getState, onAction, parentNavigationHelpers, actionCreators, setState]);
|
}, [
|
||||||
|
actionCreators,
|
||||||
|
parentNavigationHelpers,
|
||||||
|
performTransaction,
|
||||||
|
setState,
|
||||||
|
getState,
|
||||||
|
onAction,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user