From 2873ddb43c6c2b7938749bde3b40b368126fb108 Mon Sep 17 00:00:00 2001 From: "satyajit.happy" Date: Tue, 23 Jul 2019 02:54:17 +0200 Subject: [PATCH] refactor: let router specify its state shape --- example/StackNavigator.tsx | 18 ++++++---- example/TabNavigator.tsx | 15 ++++++-- src/BaseActions.tsx | 8 +++-- src/BaseRouter.tsx | 7 ++-- src/NavigationContainer.tsx | 16 +++++---- src/__tests__/__fixtures__/MockRouter.tsx | 4 +-- src/__tests__/useDescriptors.test.tsx | 33 +++++++++++------- src/__tests__/useOnAction.test.tsx | 8 ++--- src/types.tsx | 42 +++++++++++++---------- src/useDescriptors.tsx | 2 +- src/useNavigationBuilder.tsx | 16 ++++----- src/useOnAction.tsx | 2 +- src/useOnRouteFocus.tsx | 2 +- 13 files changed, 104 insertions(+), 69 deletions(-) diff --git a/example/StackNavigator.tsx b/example/StackNavigator.tsx index a5864568..d8b7ebdb 100644 --- a/example/StackNavigator.tsx +++ b/example/StackNavigator.tsx @@ -5,6 +5,7 @@ import shortid from 'shortid'; import { useNavigationBuilder, NavigationProp, + NavigationState, CommonAction, ParamListBase, Router, @@ -38,7 +39,12 @@ export type StackNavigationOptions = { export type StackNavigationProp< ParamList extends ParamListBase, RouteName extends keyof ParamList = string -> = NavigationProp & { +> = NavigationProp< + ParamList, + RouteName, + NavigationState, + StackNavigationOptions +> & { /** * Push a new screen onto the stack. * @@ -62,7 +68,7 @@ export type StackNavigationProp< popToTop(): void; }; -const StackRouter: Router = { +const StackRouter: Router = { ...BaseRouter, getInitialState({ @@ -245,10 +251,10 @@ const StackRouter: Router = { }; export function StackNavigator(props: Props) { - const { state, descriptors } = useNavigationBuilder( - StackRouter, - props - ); + const { state, descriptors } = useNavigationBuilder< + NavigationState, + StackNavigationOptions + >(StackRouter, props); return (
diff --git a/example/TabNavigator.tsx b/example/TabNavigator.tsx index a9edf514..ebd49185 100644 --- a/example/TabNavigator.tsx +++ b/example/TabNavigator.tsx @@ -10,6 +10,7 @@ import { Router, createNavigator, BaseRouter, + NavigationState, } from '../src/index'; type Props = { @@ -32,7 +33,12 @@ export type TabNavigationOptions = { export type TabNavigationProp< ParamList extends ParamListBase, RouteName extends keyof ParamList = string -> = NavigationProp & { +> = NavigationProp< + ParamList, + RouteName, + NavigationState, + TabNavigationOptions +> & { /** * Jump to an existing tab. * @@ -46,7 +52,7 @@ export type TabNavigationProp< ): void; }; -const TabRouter: Router = { +const TabRouter: Router = { ...BaseRouter, getInitialState({ @@ -169,7 +175,10 @@ const TabRouter: Router = { }; export function TabNavigator(props: Props) { - const { state, descriptors } = useNavigationBuilder(TabRouter, props); + const { state, descriptors } = useNavigationBuilder< + NavigationState, + TabNavigationOptions + >(TabRouter, props); return (
diff --git a/src/BaseActions.tsx b/src/BaseActions.tsx index 535711c0..78d73035 100644 --- a/src/BaseActions.tsx +++ b/src/BaseActions.tsx @@ -1,4 +1,4 @@ -import { PartialState, TargetRoute } from './types'; +import { PartialState, NavigationState, TargetRoute } from './types'; export type Action = | { type: 'GO_BACK' } @@ -14,7 +14,7 @@ export type Action = } | { type: 'RESET'; - payload: PartialState & { key?: string }; + payload: PartialState & { key?: string }; } | { type: 'SET_PARAMS'; @@ -47,7 +47,9 @@ export function replace(name: string, params?: object): Action { return { type: 'REPLACE', payload: { name, params } }; } -export function reset(state: PartialState & { key?: string }): Action { +export function reset( + state: PartialState & { key?: string } +): Action { return { type: 'RESET', payload: state }; } diff --git a/src/BaseRouter.tsx b/src/BaseRouter.tsx index 03eea0f7..862c959d 100644 --- a/src/BaseRouter.tsx +++ b/src/BaseRouter.tsx @@ -2,7 +2,10 @@ import shortid from 'shortid'; import { CommonAction, NavigationState } from './types'; const BaseRouter = { - getStateForAction(state: NavigationState, action: CommonAction) { + getStateForAction( + state: State, + action: CommonAction + ): State | null { switch (action.type) { case 'REPLACE': { return { @@ -35,7 +38,7 @@ const BaseRouter = { action.payload.key === state.key ) { return { - ...action.payload, + ...(action.payload as any), key: state.key, routeNames: state.routeNames, }; diff --git a/src/NavigationContainer.tsx b/src/NavigationContainer.tsx index c87ac5dc..d5e77c04 100644 --- a/src/NavigationContainer.tsx +++ b/src/NavigationContainer.tsx @@ -4,18 +4,20 @@ import { Route, NavigationState, InitialState, PartialState } from './types'; type Props = { initialState?: InitialState; - onStateChange?: (state: NavigationState | PartialState | undefined) => void; + onStateChange?: ( + state: NavigationState | PartialState | undefined + ) => void; children: React.ReactNode; }; -type State = NavigationState | PartialState | undefined; +type State = NavigationState | PartialState | undefined; const MISSING_CONTEXT_ERROR = "We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"; export const NavigationStateContext = React.createContext<{ - state?: NavigationState | PartialState; - getState: () => NavigationState | PartialState | undefined; + state?: NavigationState | PartialState; + getState: () => NavigationState | PartialState | undefined; setState: (state: NavigationState | undefined) => void; key?: string; performTransaction: (action: () => void) => void; @@ -33,7 +35,7 @@ export const NavigationStateContext = React.createContext<{ const getPartialState = ( state: InitialState | undefined -): PartialState | undefined => { +): PartialState | undefined => { if (state === undefined) { return; } @@ -46,7 +48,9 @@ const getPartialState = ( routeNames: undefined, routes: state.routes.map(route => { if (route.state === undefined) { - return route as Route & { state?: PartialState }; + return route as Route & { + state?: PartialState; + }; } return { ...route, state: getPartialState(route.state) }; diff --git a/src/__tests__/__fixtures__/MockRouter.tsx b/src/__tests__/__fixtures__/MockRouter.tsx index aa14680a..604abb63 100644 --- a/src/__tests__/__fixtures__/MockRouter.tsx +++ b/src/__tests__/__fixtures__/MockRouter.tsx @@ -1,11 +1,11 @@ -import { Router, CommonAction } from '../../types'; +import { Router, CommonAction, NavigationState } from '../../types'; import { BaseRouter } from '../../index'; export type MockActions = CommonAction & { type: 'NOOP' | 'REVERSE' | 'UPDATE'; }; -const MockRouter: Router & { key: number } = { +const MockRouter: Router & { key: number } = { key: 0, getInitialState({ diff --git a/src/__tests__/useDescriptors.test.tsx b/src/__tests__/useDescriptors.test.tsx index e6e3214f..10e2ea5a 100644 --- a/src/__tests__/useDescriptors.test.tsx +++ b/src/__tests__/useDescriptors.test.tsx @@ -4,6 +4,7 @@ import useNavigationBuilder from '../useNavigationBuilder'; import NavigationContainer from '../NavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; +import { NavigationState } from '../types'; jest.useFakeTimers(); @@ -11,10 +12,10 @@ beforeEach(() => (MockRouter.key = 0)); it('sets options with options prop as an object', () => { const TestNavigator = (props: any) => { - const { state, descriptors } = useNavigationBuilder<{ title?: string }>( - MockRouter, - props - ); + const { state, descriptors } = useNavigationBuilder< + NavigationState, + { title?: string } + >(MockRouter, props); const { render, options } = descriptors[state.routes[state.index].key]; return ( @@ -54,10 +55,10 @@ it('sets options with options prop as an object', () => { it('sets options with options prop as a fuction', () => { const TestNavigator = (props: any) => { - const { state, descriptors } = useNavigationBuilder<{ title?: string }>( - MockRouter, - props - ); + const { state, descriptors } = useNavigationBuilder< + NavigationState, + { title?: string } + >(MockRouter, props); const { render, options } = descriptors[state.routes[state.index].key]; return ( @@ -98,10 +99,13 @@ it('sets options with options prop as a fuction', () => { it('sets initial options with setOptions', () => { const TestNavigator = (props: any) => { - const { state, descriptors } = useNavigationBuilder<{ - title?: string; - color?: string; - }>(MockRouter, props); + const { state, descriptors } = useNavigationBuilder< + NavigationState, + { + title?: string; + color?: string; + } + >(MockRouter, props); const { render, options } = descriptors[state.routes[state.index].key]; return ( @@ -147,7 +151,10 @@ it('sets initial options with setOptions', () => { it('updates options with setOptions', () => { const TestNavigator = (props: any) => { - const { state, descriptors } = useNavigationBuilder(MockRouter, props); + const { state, descriptors } = useNavigationBuilder( + MockRouter, + props + ); const { render, options } = descriptors[state.routes[state.index].key]; return ( diff --git a/src/__tests__/useOnAction.test.tsx b/src/__tests__/useOnAction.test.tsx index 243e751e..8875019d 100644 --- a/src/__tests__/useOnAction.test.tsx +++ b/src/__tests__/useOnAction.test.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import { render } from 'react-native-testing-library'; -import { Router } from '../types'; import useNavigationBuilder from '../useNavigationBuilder'; import NavigationContainer from '../NavigationContainer'; import Screen from '../Screen'; import MockRouter, { MockActions } from './__fixtures__/MockRouter'; +import { Router, NavigationState } from '../types'; beforeEach(() => (MockRouter.key = 0)); it("lets parent handle the action if child didn't", () => { - const ParentRouter: Router = { + const ParentRouter: Router = { ...MockRouter, getStateForAction(state, action) { @@ -78,7 +78,7 @@ it("lets parent handle the action if child didn't", () => { }); it("lets children handle the action if parent didn't", () => { - const ParentRouter: Router = { + const ParentRouter: Router = { ...MockRouter, shouldActionPropagateToChildren() { @@ -86,7 +86,7 @@ it("lets children handle the action if parent didn't", () => { }, }; - const ChildRouter: Router = { + const ChildRouter: Router = { ...MockRouter, shouldActionChangeFocus() { diff --git a/src/types.tsx b/src/types.tsx index 546a6574..00c34e88 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -23,7 +23,9 @@ export type NavigationState = { /** * List of rendered routes. */ - routes: Array & { state?: NavigationState | PartialState }>; + routes: Array< + Route & { state?: NavigationState | PartialState } + >; /** * Whether the navigation state has been rehydrated. */ @@ -39,7 +41,7 @@ export type InitialState = Omit< routes: Array & { state?: InitialState }>; }; -export type PartialState = NavigationState & { +export type PartialState = State & { stale: true; key?: undefined; routeNames?: undefined; @@ -68,7 +70,10 @@ export type ActionCreators = { [key: string]: (...args: any) => Action; }; -export type Router = { +export type Router< + State extends NavigationState, + Action extends NavigationAction +> = { /** * Initialize the navigation state. * @@ -80,7 +85,7 @@ export type Router = { routeNames: string[]; initialRouteName: string; initialParamsList: ParamListBase; - }): NavigationState; + }): State; /** * Rehydrate the full navigation state from a given partial state. @@ -90,8 +95,8 @@ export type Router = { */ getRehydratedState(options: { routeNames: string[]; - partialState: NavigationState | PartialState; - }): NavigationState; + partialState: State | PartialState; + }): State; /** * Take the current state and updated list of route names, and return a new state. @@ -102,13 +107,13 @@ export type Router = { * @param options.initialParamsList Object containing initial params for each route. */ getStateForRouteNamesChange( - state: NavigationState, + state: State, options: { routeNames: string[]; initialRouteName: string; initialParamsList: ParamListBase; } - ): NavigationState; + ): State; /** * Take the current state and key of a route, and return a new state with the route focused @@ -116,7 +121,7 @@ export type Router = { * @param state State object to apply the action on. * @param key Key of the route to focus. */ - getStateForRouteFocus(state: NavigationState, key: string): NavigationState; + getStateForRouteFocus(state: State, key: string): State; /** * Take the current state and action, and return a new state. @@ -125,10 +130,7 @@ export type Router = { * @param state State object to apply the action on. * @param action Action object to apply. */ - getStateForAction( - state: NavigationState, - action: Action - ): NavigationState | null; + getStateForAction(state: State, action: Action): State | null; /** * Whether the action bubbles to other navigators @@ -162,16 +164,17 @@ class PrivateValueStore { private __private_value_type?: T; } -type NavigationHelpersCommon = { +type NavigationHelpersCommon< + ParamList extends ParamListBase, + State extends NavigationState = NavigationState +> = { /** * Dispatch an action or an update function to the router. * The update function will receive the current state, * * @param action Action object or update function. */ - dispatch( - action: NavigationAction | ((state: NavigationState) => NavigationState) - ): void; + dispatch(action: NavigationAction | ((state: State) => State)): void; /** * Navigate to a route in current navigation tree. @@ -203,7 +206,7 @@ type NavigationHelpersCommon = { * * @param state Navigation state object. */ - reset(state: PartialState & { key?: string }): void; + reset(state: PartialState & { key?: string }): void; /** * Go back to the previous route in history. @@ -230,8 +233,9 @@ export type NavigationHelpers< export type NavigationProp< ParamList extends ParamListBase, RouteName extends keyof ParamList = string, + State extends NavigationState = NavigationState, ScreenOptions extends object = {} -> = NavigationHelpersCommon & { +> = NavigationHelpersCommon & { /** * Update the param object for the route. * The new params will be shallow merged with the old one. diff --git a/src/useDescriptors.tsx b/src/useDescriptors.tsx index 174bb597..20543d19 100644 --- a/src/useDescriptors.tsx +++ b/src/useDescriptors.tsx @@ -14,7 +14,7 @@ import NavigationBuilderContext, { } from './NavigationBuilderContext'; type Options = { - state: NavigationState | PartialState; + state: NavigationState | PartialState; screens: { [key: string]: RouteConfig }; navigation: NavigationHelpers; onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean; diff --git a/src/useNavigationBuilder.tsx b/src/useNavigationBuilder.tsx index 314a2716..9356d164 100644 --- a/src/useNavigationBuilder.tsx +++ b/src/useNavigationBuilder.tsx @@ -5,9 +5,9 @@ import useRegisterNavigator from './useRegisterNavigator'; import useDescriptors from './useDescriptors'; import useNavigationHelpers from './useNavigationHelpers'; import useOnAction from './useOnAction'; -import { Router, NavigationState, RouteConfig, ParamListBase } from './types'; import useOnRouteFocus from './useOnRouteFocus'; import useChildActionListeners from './useChildActionListeners'; +import { Router, NavigationState, RouteConfig, ParamListBase } from './types'; type Options = { initialRouteName?: string; @@ -49,10 +49,10 @@ const getRouteConfigsFromChildren = ( ); }, []); -export default function useNavigationBuilder( - router: Router, - options: Options -) { +export default function useNavigationBuilder< + State extends NavigationState, + ScreenOptions extends object +>(router: Router, options: Options) { useRegisterNavigator(); const screens = getRouteConfigsFromChildren( @@ -96,7 +96,7 @@ export default function useNavigationBuilder( let state = router.getRehydratedState({ routeNames, - partialState: currentState, + partialState: currentState as any, }); if (!isArrayEqual(state.routeNames, routeNames)) { @@ -130,10 +130,10 @@ export default function useNavigationBuilder( }, []); const getState = React.useCallback( - (): NavigationState => + (): State => router.getRehydratedState({ routeNames, - partialState: getCurrentState() || state, + partialState: (getCurrentState() as any) || state, }), // eslint-disable-next-line react-hooks/exhaustive-deps [getCurrentState, router.getRehydratedState, router.getInitialState] diff --git a/src/useOnAction.tsx b/src/useOnAction.tsx index 8dc4eb8b..8d24921c 100644 --- a/src/useOnAction.tsx +++ b/src/useOnAction.tsx @@ -5,7 +5,7 @@ import NavigationBuilderContext, { import { NavigationAction, NavigationState, Router } from './types'; type Options = { - router: Router; + router: Router; key?: string; getState: () => NavigationState; setState: (state: NavigationState) => void; diff --git a/src/useOnRouteFocus.tsx b/src/useOnRouteFocus.tsx index 664c140c..15b3fef6 100644 --- a/src/useOnRouteFocus.tsx +++ b/src/useOnRouteFocus.tsx @@ -3,7 +3,7 @@ import { NavigationAction, NavigationState, Router } from './types'; import NavigationBuilderContext from './NavigationBuilderContext'; type Options = { - router: Router; + router: Router; onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean; getState: () => NavigationState; setState: (state: NavigationState) => void;