mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-30 05:15:25 +08:00
refactor: move types and base router to routers package
This commit is contained in:
@@ -30,7 +30,6 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.0.0",
|
||||
"shortid": "^2.2.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
45
packages/routers/src/BaseRouter.tsx
Normal file
45
packages/routers/src/BaseRouter.tsx
Normal 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;
|
||||
62
packages/routers/src/CommonActions.tsx
Normal file
62
packages/routers/src/CommonActions.tsx
Normal 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 } };
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
85
packages/routers/src/__tests__/BaseRouter.test.tsx
Normal file
85
packages/routers/src/__tests__/BaseRouter.test.tsx
Normal 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 });
|
||||
});
|
||||
8
packages/routers/src/__tests__/CommonActions.test.tsx
Normal file
8
packages/routers/src/__tests__/CommonActions.test.tsx
Normal 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'
|
||||
);
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CommonActions } from '@react-navigation/core';
|
||||
import { StackRouter, StackActions } from '../src';
|
||||
import { CommonActions, StackRouter, StackActions } from '..';
|
||||
|
||||
jest.mock('shortid', () => () => 'test');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
192
packages/routers/src/types.tsx
Normal file
192
packages/routers/src/types.tsx
Normal 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>;
|
||||
};
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user