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

View File

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

View File

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

View File

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

View File

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

View File

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