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