mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 09:30:30 +08:00
fix: don't perform side-effects in setState
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
NavigationState,
|
||||
NavigationProp,
|
||||
CommonAction,
|
||||
InitialState,
|
||||
} from '../src/index';
|
||||
|
||||
type Props = {
|
||||
@@ -31,12 +32,11 @@ const StackRouter = {
|
||||
}: {
|
||||
routeNames: string[];
|
||||
initialRouteName?: string;
|
||||
}): NavigationState {
|
||||
}): InitialState {
|
||||
const index = routeNames.indexOf(initialRouteName);
|
||||
|
||||
return {
|
||||
index,
|
||||
names: routeNames,
|
||||
routes: routeNames.slice(0, index + 1).map(name => ({
|
||||
name,
|
||||
key: `${name}-${shortid()}`,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NavigationState,
|
||||
NavigationProp,
|
||||
CommonAction,
|
||||
InitialState,
|
||||
} from '../src/index';
|
||||
|
||||
type Props = {
|
||||
@@ -29,12 +30,11 @@ const TabRouter = {
|
||||
}: {
|
||||
routeNames: string[];
|
||||
initialRouteName?: string;
|
||||
}): NavigationState {
|
||||
}): InitialState {
|
||||
const index = routeNames.indexOf(initialRouteName);
|
||||
|
||||
return {
|
||||
index,
|
||||
names: routeNames,
|
||||
routes: routeNames.map(name => ({
|
||||
name,
|
||||
key: `${name}-${shortid()}`,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationState } from './types';
|
||||
import { NavigationState, InitialState } from './types';
|
||||
|
||||
type Props = {
|
||||
initialState?: NavigationState;
|
||||
initialState?: InitialState;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -10,10 +10,11 @@ const MISSING_CONTEXT_ERROR =
|
||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?";
|
||||
|
||||
export const NavigationStateContext = React.createContext<{
|
||||
state?: NavigationState;
|
||||
setState: React.Dispatch<React.SetStateAction<NavigationState | undefined>>;
|
||||
state?: NavigationState | InitialState;
|
||||
getState: () => NavigationState | InitialState | undefined;
|
||||
setState: (state: NavigationState) => void;
|
||||
}>({
|
||||
get state(): any {
|
||||
get getState(): any {
|
||||
throw new Error(MISSING_CONTEXT_ERROR);
|
||||
},
|
||||
get setState(): any {
|
||||
@@ -22,10 +23,21 @@ export const NavigationStateContext = React.createContext<{
|
||||
});
|
||||
|
||||
export default function NavigationContainer({ initialState, children }: Props) {
|
||||
const [state, setState] = React.useState<NavigationState | undefined>(
|
||||
initialState
|
||||
);
|
||||
const value = React.useMemo(() => ({ state, setState }), [state, setState]);
|
||||
const [state, setState] = React.useState<
|
||||
NavigationState | InitialState | undefined
|
||||
>(initialState);
|
||||
|
||||
const stateRef = React.useRef(state);
|
||||
|
||||
React.useEffect(() => {
|
||||
stateRef.current = state;
|
||||
});
|
||||
|
||||
const getState = React.useCallback(() => stateRef.current, []);
|
||||
const value = React.useMemo(() => ({ state, getState, setState }), [
|
||||
getState,
|
||||
state,
|
||||
]);
|
||||
|
||||
return (
|
||||
<NavigationStateContext.Provider value={value}>
|
||||
|
||||
@@ -8,12 +8,12 @@ type Props = {
|
||||
screen: ScreenProps;
|
||||
helpers: NavigationHelpers;
|
||||
route: Route & { state?: NavigationState };
|
||||
initialState: NavigationState;
|
||||
setState: React.Dispatch<React.SetStateAction<NavigationState | undefined>>;
|
||||
getState: () => NavigationState;
|
||||
setState: (state: NavigationState) => void;
|
||||
};
|
||||
|
||||
export default function SceneView(props: Props) {
|
||||
const { screen, route, helpers, initialState, setState } = props;
|
||||
const { screen, route, helpers, getState, setState } = props;
|
||||
|
||||
const navigation = React.useMemo(
|
||||
() => ({
|
||||
@@ -23,26 +23,34 @@ export default function SceneView(props: Props) {
|
||||
[helpers, route]
|
||||
);
|
||||
|
||||
const stateRef = React.useRef(route.state);
|
||||
|
||||
React.useEffect(() => {
|
||||
stateRef.current = route.state;
|
||||
});
|
||||
|
||||
const getCurrentState = React.useCallback(() => stateRef.current, []);
|
||||
const setCurrentState = React.useCallback(
|
||||
(child: NavigationState | undefined) => {
|
||||
const state = getState();
|
||||
|
||||
setState({
|
||||
...state,
|
||||
routes: state.routes.map(r =>
|
||||
r === route ? { ...route, state: child } : r
|
||||
),
|
||||
});
|
||||
},
|
||||
[getState, route, setState]
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
state: route.state,
|
||||
setState<T = NavigationState | undefined>(child: T | ((state: T) => T)) {
|
||||
setState((state: NavigationState = initialState) => ({
|
||||
...state,
|
||||
routes: state.routes.map(r =>
|
||||
r === route
|
||||
? {
|
||||
...route,
|
||||
state:
|
||||
// @ts-ignore
|
||||
typeof child === 'function' ? child(route.state) : child,
|
||||
}
|
||||
: r
|
||||
),
|
||||
}));
|
||||
},
|
||||
getState: getCurrentState,
|
||||
setState: setCurrentState,
|
||||
}),
|
||||
[initialState, route, setState]
|
||||
[getCurrentState, route.state, setCurrentState]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,11 @@ export type NavigationState = {
|
||||
routes: Array<Route & { state?: NavigationState }>;
|
||||
};
|
||||
|
||||
export type InitialState = Omit<NavigationState, 'names'> & {
|
||||
names?: undefined;
|
||||
state?: InitialState;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
name: string;
|
||||
key: string;
|
||||
@@ -22,7 +27,7 @@ export type Router<Action extends NavigationAction = NavigationAction> = {
|
||||
initial(options: {
|
||||
routeNames: string[];
|
||||
initialRouteName?: string;
|
||||
}): NavigationState;
|
||||
}): InitialState;
|
||||
reduce(
|
||||
state: NavigationState,
|
||||
action: Action | CommonAction
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from './types';
|
||||
import Screen, { Props as ScreenProps } from './Screen';
|
||||
import SceneView from './SceneView';
|
||||
import * as BaseActions from './actions';
|
||||
import * as BaseActions from './BaseActions';
|
||||
|
||||
type Options = {
|
||||
initialRouteName?: string;
|
||||
@@ -38,40 +38,70 @@ export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
|
||||
const routeNames = Object.keys(screens);
|
||||
const initialState = React.useMemo(
|
||||
() =>
|
||||
router.initial({
|
||||
routeNames: Object.keys(screens),
|
||||
() => ({
|
||||
...router.initial({
|
||||
routeNames,
|
||||
initialRouteName: options.initialRouteName,
|
||||
}),
|
||||
names: routeNames,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[options.initialRouteName, router, ...routeNames]
|
||||
);
|
||||
|
||||
const { state = initialState, setState } = React.useContext(
|
||||
NavigationStateContext
|
||||
);
|
||||
const {
|
||||
state: currentState = initialState,
|
||||
getState: getCurrentState,
|
||||
setState,
|
||||
} = React.useContext(NavigationStateContext);
|
||||
|
||||
const getState = React.useCallback(() => {
|
||||
let state = getCurrentState();
|
||||
|
||||
if (state === undefined) {
|
||||
state = initialState;
|
||||
}
|
||||
|
||||
if (state.names === undefined) {
|
||||
state = { ...state, names: routeNames };
|
||||
}
|
||||
|
||||
return state;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getCurrentState, initialState, ...routeNames]);
|
||||
|
||||
const parentNavigationHelpers = React.useContext(NavigationHelpersContext);
|
||||
|
||||
const helpers = React.useMemo((): NavigationHelpers => {
|
||||
const dispatch = (action: NavigationAction) =>
|
||||
setState((s = initialState) => {
|
||||
const result = router.reduce(s, action);
|
||||
const dispatch = (action: NavigationAction) => {
|
||||
const state = getState();
|
||||
const result = router.reduce(state, action);
|
||||
|
||||
// If router returned `null`, let the parent navigator handle it
|
||||
if (result === null && parentNavigationHelpers !== undefined) {
|
||||
// If router returned `null`, let the parent navigator handle it
|
||||
if (result === null) {
|
||||
if (parentNavigationHelpers !== undefined) {
|
||||
parentNavigationHelpers.dispatch(action);
|
||||
} else {
|
||||
throw new Error(
|
||||
`No navigators are able to handle the action "${action.type}".`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(result);
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const actions = { ...router.actions, ...BaseActions };
|
||||
const actions = {
|
||||
...router.actions,
|
||||
...BaseActions,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
...parentNavigationHelpers,
|
||||
...Object.keys(actions).reduce(
|
||||
(acc, name) => {
|
||||
// @ts-ignore
|
||||
acc[name] = (...args: any) => dispatch(actions[name](...args));
|
||||
return acc;
|
||||
},
|
||||
@@ -79,17 +109,17 @@ export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
),
|
||||
dispatch,
|
||||
};
|
||||
}, [parentNavigationHelpers, router, setState, initialState]);
|
||||
}, [router, parentNavigationHelpers, getState, setState]);
|
||||
|
||||
const navigation = React.useMemo(
|
||||
() => ({
|
||||
...helpers,
|
||||
state,
|
||||
state: currentState,
|
||||
}),
|
||||
[helpers, state]
|
||||
[helpers, currentState]
|
||||
);
|
||||
|
||||
const descriptors = state.routes.reduce(
|
||||
const descriptors = currentState.routes.reduce(
|
||||
(acc, route) => {
|
||||
const screen = screens[route.name];
|
||||
|
||||
@@ -101,7 +131,7 @@ export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
helpers={helpers}
|
||||
route={route}
|
||||
screen={screen}
|
||||
initialState={initialState}
|
||||
getState={getState}
|
||||
setState={setState}
|
||||
/>
|
||||
</NavigationHelpersContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user