refactor: move types and base router to routers package

This commit is contained in:
Satyajit Sahoo
2020-02-10 15:53:38 +01:00
parent 7160a511e6
commit 86c39d2e0e
91 changed files with 447 additions and 434 deletions

View File

@@ -30,7 +30,6 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^5.0.0",
"shortid": "^2.2.15"
},
"devDependencies": {

View File

@@ -0,0 +1,45 @@
import { CommonNavigationAction, NavigationState, PartialState } from './types';
/**
* Base router object that can be used when writing custom routers.
* This provides few helper methods to handle common actions such as `RESET`.
*/
const BaseRouter = {
getStateForAction<State extends NavigationState>(
state: State,
action: CommonNavigationAction
): State | PartialState<State> | null {
switch (action.type) {
case 'SET_PARAMS': {
const index = action.source
? state.routes.findIndex(r => r.key === action.source)
: state.index;
if (index === -1) {
return null;
}
return {
...state,
routes: state.routes.map((r, i) =>
i === index
? { ...r, params: { ...r.params, ...action.payload.params } }
: r
),
};
}
case 'RESET':
return action.payload as PartialState<State>;
default:
return null;
}
},
shouldActionChangeFocus(action: CommonNavigationAction) {
return action.type === 'NAVIGATE';
},
};
export default BaseRouter;

View File

@@ -0,0 +1,62 @@
import { NavigationState, PartialState } from './types';
export type Action =
| {
type: 'GO_BACK';
source?: string;
target?: string;
}
| {
type: 'NAVIGATE';
payload:
| { key: string; name?: undefined; params?: object }
| { name: string; key?: string; params?: object };
source?: string;
target?: string;
}
| {
type: 'RESET';
payload: PartialState<NavigationState>;
source?: string;
target?: string;
}
| {
type: 'SET_PARAMS';
payload: { params?: object };
source?: string;
target?: string;
};
export function goBack(): Action {
return { type: 'GO_BACK' };
}
export function navigate(
route:
| { key: string; params?: object }
| { name: string; key?: string; params?: object }
): Action;
export function navigate(name: string, params?: object): Action;
export function navigate(...args: any): Action {
if (typeof args[0] === 'string') {
return { type: 'NAVIGATE', payload: { name: args[0], params: args[1] } };
} else {
const payload = args[0];
if (!payload.hasOwnProperty('key') && !payload.hasOwnProperty('name')) {
throw new Error(
'While calling navigate with an object as the argument, you need to specify name or key'
);
}
return { type: 'NAVIGATE', payload };
}
}
export function reset(state: PartialState<NavigationState>): Action {
return { type: 'RESET', payload: state };
}
export function setParams(params: object): Action {
return { type: 'SET_PARAMS', payload: { params } };
}

View File

@@ -1,5 +1,5 @@
import shortid from 'shortid';
import { CommonAction, Router, PartialState } from '@react-navigation/core';
import { PartialState, CommonNavigationAction, Router } from './types';
import TabRouter, {
TabActions,
TabActionType,
@@ -72,10 +72,10 @@ const closeDrawer = (state: DrawerNavigationState): DrawerNavigationState => {
export default function DrawerRouter(
options: DrawerRouterOptions
): Router<DrawerNavigationState, DrawerActionType | CommonAction> {
): Router<DrawerNavigationState, DrawerActionType | CommonNavigationAction> {
const router = (TabRouter(options) as unknown) as Router<
DrawerNavigationState,
TabActionType | CommonAction
TabActionType | CommonNavigationAction
>;
return {

View File

@@ -1,12 +1,12 @@
import shortid from 'shortid';
import BaseRouter from './BaseRouter';
import {
NavigationState,
CommonAction,
CommonNavigationAction,
Router,
BaseRouter,
DefaultRouterOptions,
Route,
} from '@react-navigation/core';
} from './types';
export type StackActionType =
| {
@@ -58,7 +58,10 @@ export const StackActions = {
};
export default function StackRouter(options: StackRouterOptions) {
const router: Router<StackNavigationState, CommonAction | StackActionType> = {
const router: Router<
StackNavigationState,
CommonNavigationAction | StackActionType
> = {
...BaseRouter,
type: 'stack',

View File

@@ -1,13 +1,13 @@
import shortid from 'shortid';
import BaseRouter from './BaseRouter';
import {
CommonAction,
BaseRouter,
PartialState,
NavigationState,
DefaultRouterOptions,
PartialState,
CommonNavigationAction,
Router,
DefaultRouterOptions,
Route,
} from '@react-navigation/core';
} from './types';
export type TabActionType = {
type: 'JUMP_TO';
@@ -95,7 +95,10 @@ export default function TabRouter({
initialRouteName,
backBehavior = 'history',
}: TabRouterOptions) {
const router: Router<TabNavigationState, TabActionType | CommonAction> = {
const router: Router<
TabNavigationState,
TabActionType | CommonNavigationAction
> = {
...BaseRouter,
type: 'tab',

View File

@@ -0,0 +1,85 @@
import BaseRouter from '../BaseRouter';
import * as CommonActions from '../CommonActions';
jest.mock('shortid', () => () => 'test');
const STATE = {
stale: false as const,
type: 'test',
key: 'root',
index: 1,
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
{ key: 'baz', name: 'baz' },
],
routeNames: ['foo', 'bar', 'baz', 'qux'],
};
it('sets params for the focused screen with SET_PARAMS', () => {
const result = BaseRouter.getStateForAction(
STATE,
CommonActions.setParams({ answer: 42 })
);
expect(result).toEqual({
stale: false,
type: 'test',
key: 'root',
index: 1,
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
{ key: 'baz', name: 'baz' },
],
routeNames: ['foo', 'bar', 'baz', 'qux'],
});
});
it('sets params for the source screen with SET_PARAMS', () => {
const result = BaseRouter.getStateForAction(STATE, {
...CommonActions.setParams({ answer: 42 }),
source: 'foo',
});
expect(result).toEqual({
stale: false,
type: 'test',
key: 'root',
index: 1,
routes: [
{ key: 'foo', name: 'foo', params: { answer: 42 } },
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
{ key: 'baz', name: 'baz' },
],
routeNames: ['foo', 'bar', 'baz', 'qux'],
});
});
it("doesn't handle SET_PARAMS if source key isn't present", () => {
const result = BaseRouter.getStateForAction(STATE, {
...CommonActions.setParams({ answer: 42 }),
source: 'magic',
});
expect(result).toBe(null);
});
it('resets state to new state with RESET', () => {
const routes = [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
{ key: 'baz', name: 'baz' },
{ key: 'qux-1', name: 'qux' },
];
const result = BaseRouter.getStateForAction(
STATE,
CommonActions.reset({
index: 0,
routes,
})
);
expect(result).toEqual({ index: 0, routes });
});

View File

@@ -0,0 +1,8 @@
import * as CommonActions from '../CommonActions';
it('throws if NAVIGATE is called without key or name', () => {
// @ts-ignore
expect(() => CommonActions.navigate({})).toThrowError(
'While calling navigate with an object as the argument, you need to specify name or key'
);
});

View File

@@ -1,5 +1,9 @@
import { CommonActions } from '@react-navigation/core';
import { DrawerRouter, DrawerActions, DrawerNavigationState } from '../src';
import {
CommonActions,
DrawerRouter,
DrawerActions,
DrawerNavigationState,
} from '..';
jest.mock('shortid', () => () => 'test');

View File

@@ -1,5 +1,4 @@
import { CommonActions } from '@react-navigation/core';
import { StackRouter, StackActions } from '../src';
import { CommonActions, StackRouter, StackActions } from '..';
jest.mock('shortid', () => () => 'test');

View File

@@ -1,5 +1,4 @@
import { CommonActions } from '@react-navigation/core';
import { TabRouter, TabActions, TabNavigationState } from '../src';
import { CommonActions, TabRouter, TabActions, TabNavigationState } from '..';
jest.mock('shortid', () => () => 'test');

View File

@@ -1,3 +1,9 @@
import * as CommonActions from './CommonActions';
export { CommonActions };
export { default as BaseRouter } from './BaseRouter';
export {
default as StackRouter,
StackActions,
@@ -21,3 +27,5 @@ export {
DrawerRouterOptions,
DrawerNavigationState,
} from './DrawerRouter';
export * from './types';

View File

@@ -0,0 +1,192 @@
import * as CommonActions from './CommonActions';
export type CommonNavigationAction = CommonActions.Action;
export type NavigationState = {
/**
* Unique key for the navigation state.
*/
key: string;
/**
* Index of the currently focused route.
*/
index: number;
/**
* List of valid route names as defined in the screen components.
*/
routeNames: string[];
/**
* Alternative entries for history.
*/
history?: unknown[];
/**
* List of rendered routes.
*/
routes: (Route<string> & {
state?: NavigationState | PartialState<NavigationState>;
})[];
/**
* Custom type for the state, whether it's for tab, stack, drawer etc.
* During rehydration, the state will be discarded if type doesn't match with router type.
* It can also be used to detect the type of the navigator we're dealing with.
*/
type: string;
/**
* Whether the navigation state has been rehydrated.
*/
stale: false;
};
export type InitialState = Partial<
Omit<NavigationState, 'stale' | 'routes'>
> & {
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
};
export type PartialState<State extends NavigationState> = Partial<
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
> & {
stale?: true;
type?: string;
routes: (Omit<Route<string>, 'key'> & {
key?: string;
state?: InitialState;
})[];
};
export type Route<RouteName extends string> = {
/**
* Unique key for the route.
*/
key: string;
/**
* User-provided name for the route.
*/
name: RouteName;
/**
* Params for the route.
*/
params?: object;
};
export type ParamListBase = Record<string, object | undefined>;
export type NavigationAction = {
/**
* Type of the action (e.g. `NAVIGATE`)
*/
type: string;
/**
* Additional data for the action
*/
payload?: object;
/**
* Key of the route which dispatched this action.
*/
source?: string;
/**
* Key of the navigator which should handle this action.
*/
target?: string;
};
export type ActionCreators<Action extends NavigationAction> = {
[key: string]: (...args: any) => Action;
};
export type DefaultRouterOptions = {
/**
* Name of the route to focus by on initial render.
* If not specified, usually the first route is used.
*/
initialRouteName?: string;
};
export type RouterFactory<
State extends NavigationState,
Action extends NavigationAction,
RouterOptions extends DefaultRouterOptions
> = (options: RouterOptions) => Router<State, Action>;
export type RouterConfigOptions = {
routeNames: string[];
routeParamList: ParamListBase;
};
export type Router<
State extends NavigationState,
Action extends NavigationAction
> = {
/**
* Type of the router. Should match the `type` property in state.
* If the type doesn't match, the state will be discarded during rehydration.
*/
type: State['type'];
/**
* Initialize the navigation state.
*
* @param options.routeNames List of valid route names as defined in the screen components.
* @param options.routeParamsList Object containing params for each route.
*/
getInitialState(options: RouterConfigOptions): State;
/**
* Rehydrate the full navigation state from a given partial state.
*
* @param partialState Navigation state to rehydrate from.
* @param options.routeNames List of valid route names as defined in the screen components.
* @param options.routeParamsList Object containing params for each route.
*/
getRehydratedState(
partialState: PartialState<State> | State,
options: RouterConfigOptions
): State;
/**
* Take the current state and updated list of route names, and return a new state.
*
* @param state State object to update.
* @param options.routeNames New list of route names.
* @param options.routeParamsList Object containing params for each route.
*/
getStateForRouteNamesChange(
state: State,
options: RouterConfigOptions
): State;
/**
* Take the current state and key of a route, and return a new state with the route focused
*
* @param state State object to apply the action on.
* @param key Key of the route to focus.
*/
getStateForRouteFocus(state: State, key: string): State;
/**
* Take the current state and action, and return a new state.
* If the action cannot be handled, return `null`.
*
* @param state State object to apply the action on.
* @param action Action object to apply.
* @param options.routeNames List of valid route names as defined in the screen components.
* @param options.routeParamsList Object containing params for each route.
*/
getStateForAction(
state: State,
action: Action,
options: RouterConfigOptions
): State | PartialState<State> | null;
/**
* Whether the action should also change focus in parent navigator
*
* @param action Action object to check.
*/
shouldActionChangeFocus(action: NavigationAction): boolean;
/**
* Action creators for the router.
*/
actionCreators?: ActionCreators<Action>;
};

View File

@@ -1,8 +1,5 @@
{
"extends": "../../tsconfig",
"references": [
{ "path": "../core" }
],
"compilerOptions": {
"outDir": "./lib/typescript"
}