refactor: split useNavigationBuilder into more hooks

This commit is contained in:
satyajit.happy
2019-07-12 23:26:24 +02:00
parent c2a50aa27e
commit 3d865c7d2d
7 changed files with 230 additions and 128 deletions

View File

@@ -0,0 +1,9 @@
import * as React from 'react';
import { NavigationHelpers, NavigationAction } from './types';
const NavigationBuilderContext = React.createContext<{
helpers?: NavigationHelpers;
onAction?: (action: NavigationAction) => boolean;
}>({});
export default NavigationBuilderContext;

View File

@@ -46,6 +46,10 @@ export type NavigationAction = {
type: string;
};
export type ActionCreators<Action extends NavigationAction = CommonAction> = {
[key: string]: (...args: any) => Action;
};
export type Router<Action extends NavigationAction = CommonAction> = {
/**
* Initialize full navigation state with a given partial state.
@@ -68,7 +72,7 @@ export type Router<Action extends NavigationAction = CommonAction> = {
/**
* Action creators for the router.
*/
actionCreators: { [key: string]: (...args: any) => Action };
actionCreators: ActionCreators<Action>;
};
export type ParamListBase = { [key: string]: object | void };

63
src/useDescriptors.tsx Normal file
View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import {
Descriptor,
InitialState,
NavigationAction,
NavigationHelpers,
NavigationState,
ParamListBase,
ScreenProps,
} from './types';
import SceneView from './SceneView';
import NavigationBuilderContext from './NavigationBuilderContext';
type Options = {
state: NavigationState | InitialState;
screens: { [key: string]: ScreenProps<ParamListBase, string> };
helpers: NavigationHelpers<ParamListBase>;
onAction: (action: NavigationAction) => boolean;
getState: () => NavigationState;
setState: (state: NavigationState) => void;
};
export default function useDescriptors({
state,
screens,
helpers,
onAction,
getState,
setState,
}: Options) {
const context = React.useMemo(
() => ({
helpers,
onAction,
}),
[helpers, onAction]
);
return state.routes.reduce(
(acc, route) => {
const screen = screens[route.name];
acc[route.key] = {
render() {
return (
<NavigationBuilderContext.Provider value={context}>
<SceneView
helpers={helpers}
route={route}
screen={screen}
getState={getState}
setState={setState}
/>
</NavigationBuilderContext.Provider>
);
},
options: screen.options || {},
};
return acc;
},
{} as { [key: string]: Descriptor }
);
}

View File

@@ -1,51 +1,22 @@
import * as React from 'react';
import { NavigationStateContext } from './NavigationContainer';
import {
Router,
NavigationAction,
Descriptor,
NavigationHelpers,
NavigationState,
ScreenProps,
} from './types';
import Screen from './Screen';
import shortid from 'shortid';
import SceneView from './SceneView';
import * as BaseActions from './BaseActions';
import { SingleNavigatorContext } from './EnsureSingleNavigator';
import useRegisterNavigator from './useRegisterNavigator';
import useDescriptors from './useDescriptors';
import useNavigationHelpers from './useNavigationHelpers';
import useOnAction from './useOnAction';
import { Router, NavigationState, ScreenProps } from './types';
type Options = {
initialRouteName?: string;
children: React.ReactNode;
};
type HandleAction = (action: NavigationAction) => boolean;
const NavigationBuilderContext = React.createContext<{
helpers?: NavigationHelpers;
onAction?: HandleAction;
}>({});
export default function useNavigationBuilder(
router: Router<any>,
options: Options
) {
const [key] = React.useState(shortid());
const singleNavigatorContext = React.useContext(SingleNavigatorContext);
React.useEffect(() => {
if (singleNavigatorContext === undefined) {
throw new Error(
"Couldn't register the navigator. You likely forgot to nest the navigator inside a 'NavigationContainer'."
);
}
const { register, unregister } = singleNavigatorContext;
register(key);
return () => unregister(key);
}, [key, singleNavigatorContext]);
useRegisterNavigator();
const screens = React.Children.map(options.children, child => {
if (React.isValidElement(child) && child.type === Screen) {
@@ -74,11 +45,6 @@ export default function useNavigationBuilder(
setState,
} = React.useContext(NavigationStateContext);
const {
helpers: parentNavigationHelpers,
onAction: handleActionParent,
} = React.useContext(NavigationBuilderContext);
const getState = React.useCallback(
(): NavigationState =>
router.getInitialState({
@@ -90,62 +56,18 @@ export default function useNavigationBuilder(
[getCurrentState, ...routeNames]
);
const onAction = React.useCallback(
(action: NavigationAction) => {
const state = getState();
const onAction = useOnAction({
getState,
setState,
getStateForAction: router.getStateForAction,
});
const result = router.getStateForAction(state, action);
if (result !== null) {
if (state !== result) {
setState(result);
}
return true;
}
if (handleActionParent !== undefined) {
// Bubble action to the parent if the current navigator didn't handle it
if (handleActionParent(action)) {
return true;
}
}
return false;
},
[getState, handleActionParent, router, setState]
);
const helpers = React.useMemo((): NavigationHelpers => {
const dispatch = (
action: NavigationAction | ((state: NavigationState) => NavigationState)
) => {
if (typeof action === 'function') {
setState(action(getState()));
} else {
onAction(action);
}
};
const actions = {
...router.actionCreators,
...BaseActions,
};
// @ts-ignore
return {
...parentNavigationHelpers,
...Object.keys(actions).reduce(
(acc, name) => {
// @ts-ignore
acc[name] = (...args: any) => dispatch(actions[name](...args));
return acc;
},
{} as { [key: string]: () => void }
),
dispatch,
};
}, [getState, onAction, parentNavigationHelpers, router, setState]);
const helpers = useNavigationHelpers({
onAction,
getState,
setState,
actionCreators: router.actionCreators,
});
const navigation = React.useMemo(
() => ({
@@ -155,38 +77,14 @@ export default function useNavigationBuilder(
[helpers, currentState]
);
const context = React.useMemo(
() => ({
helpers,
onAction,
}),
[helpers, onAction]
);
const descriptors = currentState.routes.reduce(
(acc, route) => {
const screen = screens[route.name];
acc[route.key] = {
render() {
return (
<NavigationBuilderContext.Provider value={context}>
<SceneView
helpers={helpers}
route={route}
screen={screen}
getState={getState}
setState={setState}
/>
</NavigationBuilderContext.Provider>
);
},
options: screen.options || {},
};
return acc;
},
{} as { [key: string]: Descriptor }
);
const descriptors = useDescriptors({
state: currentState,
screens,
helpers,
onAction,
getState,
setState,
});
return {
navigation,

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import * as BaseActions from './BaseActions';
import NavigationBuilderContext from './NavigationBuilderContext';
import {
NavigationHelpers,
NavigationAction,
NavigationState,
ActionCreators,
} from './types';
type Options = {
onAction: (action: NavigationAction) => boolean;
getState: () => NavigationState;
setState: (state: NavigationState) => void;
actionCreators: ActionCreators;
};
export default function useNavigationHelpers({
onAction,
getState,
setState,
actionCreators,
}: Options) {
const { helpers: parentNavigationHelpers } = React.useContext(
NavigationBuilderContext
);
return React.useMemo((): NavigationHelpers => {
const dispatch = (
action: NavigationAction | ((state: NavigationState) => NavigationState)
) => {
if (typeof action === 'function') {
setState(action(getState()));
} else {
onAction(action);
}
};
const actions = {
...actionCreators,
...BaseActions,
};
// @ts-ignore
return {
...parentNavigationHelpers,
...Object.keys(actions).reduce(
(acc, name) => {
// @ts-ignore
acc[name] = (...args: any) => dispatch(actions[name](...args));
return acc;
},
{} as { [key: string]: () => void }
),
dispatch,
};
}, [getState, onAction, parentNavigationHelpers, actionCreators, setState]);
}

48
src/useOnAction.tsx Normal file
View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import NavigationBuilderContext from './NavigationBuilderContext';
import { NavigationAction, NavigationState } from './types';
type Options = {
getState: () => NavigationState;
setState: (state: NavigationState) => void;
getStateForAction: (
state: NavigationState,
action: NavigationAction
) => NavigationState | null;
};
export default function useOnAction({
getState,
setState,
getStateForAction,
}: Options) {
const { onAction: handleActionParent } = React.useContext(
NavigationBuilderContext
);
return React.useCallback(
(action: NavigationAction) => {
const state = getState();
const result = getStateForAction(state, action);
if (result !== null) {
if (state !== result) {
setState(result);
}
return true;
}
if (handleActionParent !== undefined) {
// Bubble action to the parent if the current navigator didn't handle it
if (handleActionParent(action)) {
return true;
}
}
return false;
},
[getState, handleActionParent, getStateForAction, setState]
);
}

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import shortid from 'shortid';
import { SingleNavigatorContext } from './EnsureSingleNavigator';
export default function useRegisterNavigator() {
const [key] = React.useState(shortid());
const singleNavigatorContext = React.useContext(SingleNavigatorContext);
React.useEffect(() => {
if (singleNavigatorContext === undefined) {
throw new Error(
"Couldn't register the navigator. You likely forgot to nest the navigator inside a 'NavigationContainer'."
);
}
const { register, unregister } = singleNavigatorContext;
register(key);
return () => unregister(key);
}, [key, singleNavigatorContext]);
}