fix: don't perform side-effects in setState

This commit is contained in:
satyajit.happy
2019-06-10 15:47:01 +02:00
parent 33fd99c842
commit 67b09da633
6 changed files with 109 additions and 54 deletions

View File

@@ -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()}`,

View File

@@ -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()}`,

View File

@@ -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}>

View File

@@ -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 (

View File

@@ -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

View File

@@ -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>