mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-06 22:39:41 +08:00
feat: add typed navigator for better typechecking
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
type Props = {
|
||||
initialRouteName?: string;
|
||||
children: React.ReactElement[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type Action =
|
||||
@@ -28,27 +28,37 @@ export type StackNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<ParamList, RouteName> & {
|
||||
/**
|
||||
* Push a new screen onto the stack.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
push<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Pop a screen from the stack.
|
||||
*/
|
||||
pop(): void;
|
||||
};
|
||||
|
||||
const StackRouter = {
|
||||
normalize({
|
||||
initial({
|
||||
screens,
|
||||
currentState,
|
||||
partialState,
|
||||
initialRouteName = Object.keys(screens)[0],
|
||||
}: {
|
||||
screens: { [key: string]: ScreenProps };
|
||||
currentState?: InitialState | NavigationState;
|
||||
partialState?: InitialState | NavigationState;
|
||||
initialRouteName?: string;
|
||||
}): NavigationState {
|
||||
const routeNames = Object.keys(screens);
|
||||
|
||||
let state = currentState;
|
||||
let state = partialState;
|
||||
|
||||
if (state === undefined) {
|
||||
const index = routeNames.indexOf(initialRouteName);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
type Props = {
|
||||
initialRouteName?: string;
|
||||
children: React.ReactElement[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type Action = {
|
||||
@@ -26,6 +26,12 @@ export type TabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<ParamList, RouteName> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
jumpTo<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
@@ -34,18 +40,18 @@ export type TabNavigationProp<
|
||||
};
|
||||
|
||||
const TabRouter = {
|
||||
normalize({
|
||||
initial({
|
||||
screens,
|
||||
currentState,
|
||||
partialState,
|
||||
initialRouteName = Object.keys(screens)[0],
|
||||
}: {
|
||||
screens: { [key: string]: ScreenProps };
|
||||
currentState?: InitialState | NavigationState;
|
||||
partialState?: InitialState | NavigationState;
|
||||
initialRouteName?: string;
|
||||
}): NavigationState {
|
||||
const routeNames = Object.keys(screens);
|
||||
|
||||
let state = currentState;
|
||||
let state = partialState;
|
||||
|
||||
if (state === undefined) {
|
||||
const index = routeNames.indexOf(initialRouteName);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import shortid from 'shortid';
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { NavigationContainer, Screen, CompositeNavigationProp } from '../src';
|
||||
import {
|
||||
NavigationContainer,
|
||||
Screen,
|
||||
CompositeNavigationProp,
|
||||
TypedNavigator,
|
||||
} from '../src';
|
||||
import StackNavigator, { StackNavigationProp } from './StackNavigator';
|
||||
import TabNavigator, { TabNavigationProp } from './TabNavigator';
|
||||
|
||||
@@ -16,6 +21,16 @@ type TabParamList = {
|
||||
fifth: void;
|
||||
};
|
||||
|
||||
const Stack: TypedNavigator<StackParamList, typeof StackNavigator> = {
|
||||
Navigator: StackNavigator,
|
||||
Screen,
|
||||
};
|
||||
|
||||
const Tab: TypedNavigator<TabParamList, typeof TabNavigator> = {
|
||||
Navigator: TabNavigator,
|
||||
Screen,
|
||||
};
|
||||
|
||||
const First = ({
|
||||
navigation,
|
||||
}: {
|
||||
@@ -125,23 +140,27 @@ const initialState = routes.length
|
||||
function App() {
|
||||
return (
|
||||
<NavigationContainer initialState={initialState}>
|
||||
<StackNavigator>
|
||||
<Screen
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name="first"
|
||||
component={First}
|
||||
options={{ title: 'Foo' }}
|
||||
initialParams={{ author: 'Jane' }}
|
||||
/>
|
||||
<Screen name="second" component={Second} options={{ title: 'Bar' }} />
|
||||
<Screen name="third" options={{ title: 'Baz' }}>
|
||||
<Stack.Screen
|
||||
name="second"
|
||||
component={Second}
|
||||
options={{ title: 'Bar' }}
|
||||
/>
|
||||
<Stack.Screen name="third" options={{ title: 'Baz' }}>
|
||||
{() => (
|
||||
<TabNavigator initialRouteName="fifth">
|
||||
<Screen name="fourth" component={Fourth} />
|
||||
<Screen name="fifth" component={Fifth} />
|
||||
</TabNavigator>
|
||||
<Tab.Navigator initialRouteName="fifth">
|
||||
<Tab.Screen name="fourth" component={Fourth} />
|
||||
<Tab.Screen name="fifth" component={Fifth} />
|
||||
</Tab.Navigator>
|
||||
)}
|
||||
</Screen>
|
||||
</StackNavigator>
|
||||
</Stack.Screen>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
138
src/types.tsx
138
src/types.tsx
@@ -3,9 +3,21 @@ import * as BaseActions from './BaseActions';
|
||||
export type CommonAction = BaseActions.Action;
|
||||
|
||||
export type NavigationState = {
|
||||
/**
|
||||
* Unique key for the navigation state.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Index of the currently focused route.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* List if valid route names.
|
||||
*/
|
||||
names: string[];
|
||||
/**
|
||||
* List of rendered routes.
|
||||
*/
|
||||
routes: Array<Route & { state?: NavigationState }>;
|
||||
};
|
||||
|
||||
@@ -15,10 +27,19 @@ export type InitialState = Omit<Omit<NavigationState, 'names'>, 'key'> & {
|
||||
state?: InitialState;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
export type Route<RouteName = string> = {
|
||||
/**
|
||||
* Unique key for the route.
|
||||
*/
|
||||
key: string;
|
||||
name: string;
|
||||
params?: object;
|
||||
/**
|
||||
* User-provided name for the route.
|
||||
*/
|
||||
name: RouteName;
|
||||
/**
|
||||
* Params for the route.
|
||||
*/
|
||||
params?: object | void;
|
||||
};
|
||||
|
||||
export type NavigationAction = {
|
||||
@@ -26,35 +47,70 @@ export type NavigationAction = {
|
||||
};
|
||||
|
||||
export type Router<Action extends NavigationAction = NavigationAction> = {
|
||||
normalize(options: {
|
||||
/**
|
||||
* Initialize full navigation state with a given partial state.
|
||||
*/
|
||||
initial(options: {
|
||||
screens: { [key: string]: ScreenProps };
|
||||
currentState?: NavigationState | InitialState;
|
||||
partialState?: NavigationState | InitialState;
|
||||
initialRouteName?: string;
|
||||
}): NavigationState;
|
||||
|
||||
/**
|
||||
* Take the current state and action, and return a new state.
|
||||
* If the action cannot be handled, return `null`.
|
||||
*/
|
||||
reduce(
|
||||
state: NavigationState,
|
||||
action: Action | CommonAction
|
||||
): NavigationState | null;
|
||||
|
||||
/**
|
||||
* Action creators for the router.
|
||||
*/
|
||||
actions: { [key: string]: (...args: any) => Action };
|
||||
};
|
||||
|
||||
export type ParamListBase = { [key: string]: object | void };
|
||||
|
||||
class PrivateValueStore<T> {
|
||||
// TypeScript requires a type to be actually used to be able to infer it.
|
||||
// This is a hacky way of storing type in a property without surfacing it in intellisense.
|
||||
/**
|
||||
* TypeScript requires a type to be actually used to be able to infer it.
|
||||
* This is a hacky way of storing type in a property without surfacing it in intellisense.
|
||||
*/
|
||||
// @ts-ignore
|
||||
private __private_value_type?: T;
|
||||
}
|
||||
|
||||
export type NavigationHelpers<ParamList extends ParamListBase = {}> = {
|
||||
export type NavigationHelpers<
|
||||
ParamList extends ParamListBase = ParamListBase
|
||||
> = {
|
||||
/**
|
||||
* Dispatch an action to the router.
|
||||
*/
|
||||
dispatch(action: NavigationAction): void;
|
||||
|
||||
/**
|
||||
* Navigate to a route in current navigation tree.
|
||||
*
|
||||
* @param name Name of the route to navigate to.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
navigate<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Reset the navigation state to the provided state.
|
||||
* If a key is provided, the state with matching key will be reset.
|
||||
*/
|
||||
reset(state: InitialState & { key?: string }): void;
|
||||
|
||||
/**
|
||||
* Go back to the previous route in history.
|
||||
*/
|
||||
goBack(): void;
|
||||
} & PrivateValueStore<ParamList>;
|
||||
|
||||
@@ -62,7 +118,11 @@ export type NavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList
|
||||
> = NavigationHelpers<ParamList> & {
|
||||
state: Route & { name: RouteName } & (ParamList[RouteName] extends void
|
||||
/**
|
||||
* State for the child navigator.
|
||||
*/
|
||||
state: Route<RouteName> &
|
||||
(ParamList[RouteName] extends void
|
||||
? never
|
||||
: { params: ParamList[RouteName] });
|
||||
};
|
||||
@@ -77,19 +137,69 @@ export type CompositeNavigationProp<
|
||||
>;
|
||||
|
||||
export type Descriptor = {
|
||||
/**
|
||||
* Render the component associated with this route.
|
||||
*/
|
||||
render(): React.ReactNode;
|
||||
|
||||
/**
|
||||
* Options for the route.
|
||||
*/
|
||||
options: Options;
|
||||
};
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
* Title text for the screen.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type ScreenProps = {
|
||||
name: string;
|
||||
export type ScreenProps<
|
||||
ParamList extends ParamListBase = ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = {
|
||||
/**
|
||||
* Route name of this screen.
|
||||
*/
|
||||
name: RouteName;
|
||||
|
||||
/**
|
||||
* Navigator options for this screen.
|
||||
*/
|
||||
options?: Options;
|
||||
initialParams?: object;
|
||||
|
||||
/**
|
||||
* Initial params object for the route.
|
||||
*/
|
||||
initialParams?: ParamList[RouteName];
|
||||
} & (
|
||||
| { component: React.ComponentType<any> }
|
||||
| { children: (props: any) => React.ReactNode });
|
||||
| {
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Render callback to render content of this screen.
|
||||
*/
|
||||
children: (props: any) => React.ReactNode;
|
||||
});
|
||||
|
||||
export type TypedNavigator<
|
||||
ParamList extends ParamListBase,
|
||||
Navigator extends React.ComponentType<any>
|
||||
> = {
|
||||
Navigator: React.ComponentType<
|
||||
React.ComponentProps<Navigator> & {
|
||||
/**
|
||||
* Route to focus on initial render.
|
||||
*/
|
||||
initialRouteName?: keyof ParamList;
|
||||
}
|
||||
>;
|
||||
Screen: React.ComponentType<ScreenProps<ParamList, keyof ParamList>>;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as BaseActions from './BaseActions';
|
||||
|
||||
type Options = {
|
||||
initialRouteName?: string;
|
||||
children: React.ReactElement[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const NavigationHelpersContext = React.createContext<
|
||||
@@ -23,13 +23,13 @@ export const NavigationHelpersContext = React.createContext<
|
||||
|
||||
export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
const screens = React.Children.map(options.children, child => {
|
||||
if (child.type !== Screen) {
|
||||
throw new Error(
|
||||
"A navigator can only contain 'Screen' components as its direct children"
|
||||
);
|
||||
if (React.isValidElement(child) && child.type === Screen) {
|
||||
return child.props as ScreenProps;
|
||||
}
|
||||
|
||||
return child.props as ScreenProps;
|
||||
throw new Error(
|
||||
"A navigator can only contain 'Screen' components as its direct children"
|
||||
);
|
||||
}).reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.name] = curr;
|
||||
@@ -41,7 +41,7 @@ export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
const routeNames = Object.keys(screens);
|
||||
|
||||
const {
|
||||
state: currentState = router.normalize({
|
||||
state: currentState = router.initial({
|
||||
screens,
|
||||
initialRouteName: options.initialRouteName,
|
||||
}),
|
||||
@@ -51,9 +51,9 @@ export default function useNavigationBuilder(router: Router, options: Options) {
|
||||
|
||||
const getState = React.useCallback(
|
||||
(): NavigationState =>
|
||||
router.normalize({
|
||||
router.initial({
|
||||
screens,
|
||||
currentState: getCurrentState(),
|
||||
partialState: getCurrentState(),
|
||||
initialRouteName: options.initialRouteName,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
Reference in New Issue
Block a user