chore: initial POC

This commit is contained in:
satyajit.happy
2019-06-09 19:37:23 +02:00
parent 09e1edf510
commit fc1a4adc74
11 changed files with 344 additions and 6 deletions

View File

@@ -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
View 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>
);
}

View File

@@ -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'));

View File

@@ -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",

View 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
View 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;
}

View File

6
src/index.tsx Normal file
View 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
View 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;
};

View 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 };
}

View File

@@ -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"