mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 12:25:21 +08:00
chore: initial POC
This commit is contained in:
@@ -14,10 +14,6 @@ An exploration of a component-first API for React Navigation for building more d
|
||||
|
||||
Component which wraps the whole app. It stores the state for the whole navigation tree.
|
||||
|
||||
### `NavigationProvider`
|
||||
|
||||
Component which will hold the navigation state. It's usually rendered at the root by the user. Navigators also need to use this internally to support nested navigation states.
|
||||
|
||||
### `useNavigationBuilder`
|
||||
|
||||
Hook which can access the navigation state from the context. Along with the state, it also provides some helpers to modify the navigation state provided by the router. All state changes are notified to the parent `NavigationContainer`.
|
||||
|
||||
102
example/StackNavigator.tsx
Normal file
102
example/StackNavigator.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable react-native/no-inline-styles */
|
||||
|
||||
import * as React from 'react';
|
||||
import shortid from 'shortid';
|
||||
import {
|
||||
useNavigationBuilder,
|
||||
NavigationState,
|
||||
NavigationProp,
|
||||
} from '../src/index';
|
||||
|
||||
type Props = {
|
||||
initialRouteName?: string;
|
||||
children: React.ReactElement[];
|
||||
};
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'PUSH';
|
||||
payload: { name: string };
|
||||
}
|
||||
| { type: 'POP' };
|
||||
|
||||
export type StackNavigationProp = NavigationProp<typeof StackRouter>;
|
||||
|
||||
const StackRouter = {
|
||||
getInitialState(
|
||||
routeNames: string[],
|
||||
{ initialRouteName = routeNames[0] }: { initialRouteName?: string }
|
||||
) {
|
||||
const index = routeNames.indexOf(initialRouteName);
|
||||
|
||||
return {
|
||||
index,
|
||||
routes: routeNames.slice(0, index + 1).map(name => ({
|
||||
name,
|
||||
key: `${name}-${shortid()}`,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
reduce(state: NavigationState, action: Action) {
|
||||
switch (action.type) {
|
||||
case 'PUSH':
|
||||
return {
|
||||
index: state.index + 1,
|
||||
routes: [
|
||||
...state.routes,
|
||||
{
|
||||
name: action.payload.name,
|
||||
key: `${name}-${shortid()}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'POP':
|
||||
return state.index > 0
|
||||
? {
|
||||
index: state.index - 1,
|
||||
routes: state.routes.slice(0, state.routes.length - 1),
|
||||
}
|
||||
: state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
push(name: string): Action {
|
||||
return { type: 'PUSH', payload: { name } };
|
||||
},
|
||||
pop(): Action {
|
||||
return { type: 'POP' };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function StackNavigator({ initialRouteName, children }: Props) {
|
||||
// The `navigation` object contains the navigation state and some helpers (e.g. push, pop)
|
||||
// The `descriptors` object contains `navigation` objects for children routes and helper for rendering a screen
|
||||
const { navigation, descriptors } = useNavigationBuilder(StackRouter, {
|
||||
initialRouteName,
|
||||
children,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{navigation.state.routes.map((route, i) => (
|
||||
<div
|
||||
key={route.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: i * 10,
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
}}
|
||||
>
|
||||
{descriptors[route.name].render()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { NavigationContainer, Screen } from '../src';
|
||||
import StackNavigator, { StackNavigationProp } from './StackNavigator';
|
||||
|
||||
const First = ({ navigation }: { navigation: StackNavigationProp }) => (
|
||||
<div>
|
||||
<h1>First route</h1>
|
||||
<button type="button" onClick={() => navigation.push('second')}>
|
||||
Push second
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Go back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Second = ({ navigation }: { navigation: StackNavigationProp }) => (
|
||||
<div>
|
||||
<h1>Second route</h1>
|
||||
<button type="button" onClick={() => navigation.push('first')}>
|
||||
Push first
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Go back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return <h1>Hello world</h1>;
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<StackNavigator>
|
||||
<Screen name="first" component={First} />
|
||||
<Screen name="second" component={Second} />
|
||||
</StackNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
render(<App />, document.getElementById('root'));
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.1.3"
|
||||
"core-js": "^3.1.3",
|
||||
"shortid": "^2.2.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
@@ -45,6 +46,7 @@
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/react": "^16.8.19",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"codecov": "^3.5.0",
|
||||
"commitlint": "^8.0.0",
|
||||
"del-cli": "^2.0.0",
|
||||
|
||||
36
src/NavigationContainer.tsx
Normal file
36
src/NavigationContainer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationState } from './types';
|
||||
|
||||
type Props = {
|
||||
initialState?: NavigationState;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const MISSING_CONTEXT_ERROR =
|
||||
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?";
|
||||
|
||||
export const NavigationContext = React.createContext<{
|
||||
state?: NavigationState;
|
||||
setState: (state: NavigationState) => void;
|
||||
}>({
|
||||
get state(): any {
|
||||
throw new Error(MISSING_CONTEXT_ERROR);
|
||||
},
|
||||
get setState(): any {
|
||||
throw new Error(MISSING_CONTEXT_ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
export default function NavigationContainer({ initialState, children }: Props) {
|
||||
const [state, setState] = React.useState<NavigationState | undefined>(
|
||||
initialState
|
||||
);
|
||||
|
||||
const value = React.useMemo(() => ({ state, setState }), [state, setState]);
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={value}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
10
src/Screen.tsx
Normal file
10
src/Screen.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Props = {
|
||||
name: string;
|
||||
options?: object;
|
||||
} & (
|
||||
| { component: React.ComponentType<any> }
|
||||
| { children: (props: any) => React.ReactNode });
|
||||
|
||||
export default function Screen(_: Props) {
|
||||
return null;
|
||||
}
|
||||
6
src/index.tsx
Normal file
6
src/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as NavigationContainer } from './NavigationContainer';
|
||||
export { default as Screen } from './Screen';
|
||||
|
||||
export { default as useNavigationBuilder } from './useNavigationBuilder';
|
||||
|
||||
export * from './types';
|
||||
37
src/types.tsx
Normal file
37
src/types.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
export type NavigationState = {
|
||||
index: number;
|
||||
routes: Array<Route | Route & NavigationState>;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
name: string;
|
||||
key: string;
|
||||
params?: {};
|
||||
};
|
||||
|
||||
export type NavigationAction = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type Router<Action extends NavigationAction = NavigationAction> = {
|
||||
getInitialState(
|
||||
routeNames: string[],
|
||||
options: { initialRouteName?: string }
|
||||
): NavigationState;
|
||||
reduce(state: NavigationState, action: Action): NavigationState;
|
||||
actions: { [key: string]: (...args: any) => Action };
|
||||
};
|
||||
|
||||
export type NavigationProp<
|
||||
T extends { actions: Router['actions'] } = { actions: {} }
|
||||
> = {
|
||||
state: Route | NavigationState;
|
||||
dispatch: (action: NavigationAction) => void;
|
||||
} & {
|
||||
[key in keyof T['actions']]: (...args: Parameters<T['actions'][key]>) => void;
|
||||
};
|
||||
|
||||
export type Descriptor<Options = {}> = {
|
||||
render(): React.ReactNode;
|
||||
options: Options;
|
||||
};
|
||||
99
src/useNavigationBuilder.tsx
Normal file
99
src/useNavigationBuilder.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationContext } from './NavigationContainer';
|
||||
import {
|
||||
Router,
|
||||
NavigationAction,
|
||||
NavigationProp,
|
||||
Descriptor,
|
||||
NavigationState,
|
||||
} from './types';
|
||||
import Screen, { Props as ScreenProps } from './Screen';
|
||||
|
||||
type Options = {
|
||||
initialRouteName?: string;
|
||||
children: React.ReactElement[];
|
||||
};
|
||||
|
||||
export default function useNavigationBuilder(
|
||||
router: Router,
|
||||
{ initialRouteName, children }: Options
|
||||
) {
|
||||
const screens = React.Children.map(children, child => {
|
||||
if (child.type !== Screen) {
|
||||
throw new Error(
|
||||
"A navigator can only contain 'Screen' components as its direct children"
|
||||
);
|
||||
}
|
||||
|
||||
return child.props as ScreenProps;
|
||||
}).reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.name] = curr;
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: ScreenProps }
|
||||
);
|
||||
|
||||
const routeNames = Object.keys(screens);
|
||||
|
||||
const {
|
||||
state = router.getInitialState(routeNames, { initialRouteName }),
|
||||
setState,
|
||||
} = React.useContext(NavigationContext);
|
||||
|
||||
const navigation = React.useMemo((): NavigationProp & {
|
||||
state: NavigationState;
|
||||
} => {
|
||||
const dispatch = (action: NavigationAction) =>
|
||||
setState(router.reduce(state, action));
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
...Object.keys(router.actions).reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = (...args: any) => dispatch(router.actions[name](...args));
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: () => void }
|
||||
),
|
||||
};
|
||||
}, [state, router, setState]);
|
||||
|
||||
const descriptors = state.routes.reduce(
|
||||
(acc, route) => {
|
||||
const screen = screens[route.name];
|
||||
const nav = {
|
||||
...navigation,
|
||||
state: route,
|
||||
};
|
||||
|
||||
acc[route.name] = {
|
||||
render: () => (
|
||||
<NavigationContext.Provider
|
||||
value={{
|
||||
state: 'routes' in route && 'index' in route ? route : undefined,
|
||||
setState: s =>
|
||||
setState({
|
||||
...state,
|
||||
// @ts-ignore
|
||||
routes: state.routes.map(r => (r === route ? s : r)),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{'component' in screen && screen.component !== undefined ? (
|
||||
<screen.component navigation={nav} />
|
||||
) : 'children' in screen && screen.children !== undefined ? (
|
||||
screen.children({ navigation: nav })
|
||||
) : null}
|
||||
</NavigationContext.Provider>
|
||||
),
|
||||
options: screen.options || {},
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: Descriptor }
|
||||
);
|
||||
|
||||
return { navigation, descriptors };
|
||||
}
|
||||
17
yarn.lock
17
yarn.lock
@@ -1365,6 +1365,11 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/shortid@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b"
|
||||
integrity sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=
|
||||
|
||||
"@types/stack-utils@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||
@@ -6415,6 +6420,11 @@ nan@^2.12.1:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||
|
||||
nanoid@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.0.3.tgz#dde999e173bc9d7bd2ee2746b89909ade98e075e"
|
||||
integrity sha512-NbaoqdhIYmY6FXDRB4eYtDVC9Z9eCbn8TyaiC16LNKtpPv/aqa0tOPD8y6gNE4yUNnaZ7LLhYtXOev/6+cBtfw==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
@@ -8563,6 +8573,13 @@ shellwords@^0.1.1:
|
||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||
|
||||
shortid@^2.2.14:
|
||||
version "2.2.14"
|
||||
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.14.tgz#80db6aafcbc3e3a46850b3c88d39e051b84c8d18"
|
||||
integrity sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==
|
||||
dependencies:
|
||||
nanoid "^2.0.0"
|
||||
|
||||
sigmund@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||
|
||||
Reference in New Issue
Block a user