diff --git a/example/StackNavigator.tsx b/example/StackNavigator.tsx index 9a9664d1e3ece4..d2bf76294f06eb 100644 --- a/example/StackNavigator.tsx +++ b/example/StackNavigator.tsx @@ -88,6 +88,14 @@ const StackRouter: Router = { return state; }, + getStateForRouteNamesChange(state, { routeNames }) { + return { + ...state, + routeNames, + routes: state.routes.filter(route => routeNames.includes(route.name)), + }; + }, + getStateForAction(state, action) { switch (action.type) { case 'PUSH': diff --git a/example/TabNavigator.tsx b/example/TabNavigator.tsx index cdccdb4b8776dd..ed5db03fcf83c4 100644 --- a/example/TabNavigator.tsx +++ b/example/TabNavigator.tsx @@ -72,6 +72,21 @@ const TabRouter: Router = { return state; }, + getStateForRouteNamesChange(state, { routeNames, initialParamsList }) { + return { + ...state, + routeNames, + routes: routeNames.map( + name => + state.routes.find(r => r.name === name) || { + name, + key: `${name}-${shortid()}`, + params: initialParamsList[name], + } + ), + }; + }, + getStateForAction(state, action) { switch (action.type) { case 'JUMP_TO': @@ -190,4 +205,4 @@ export function TabNavigator(props: Props) { ); } -export default createNavigator(TabNavigator); \ No newline at end of file +export default createNavigator(TabNavigator); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index daaf6da50d5c93..1ea555c2371c64 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -41,6 +41,14 @@ export const MockRouter: Router<{ type: string }> & { key: number } = { return state; }, + getStateForRouteNamesChange(state, { routeNames }) { + return { + ...state, + routeNames, + routes: state.routes.filter(route => routeNames.includes(route.name)), + }; + }, + getStateForAction(state, action) { switch (action.type) { case 'UPDATE': @@ -535,6 +543,41 @@ it('updates route params with setParams', () => { }); }); +it('handles change in route names', () => { + const TestNavigator = (props: any): any => { + useNavigationBuilder(MockRouter, props); + return null; + }; + + const onStateChange = jest.fn(); + + const root = render( + + + + + + + ); + + root.update( + + + + + + + + ); + + expect(onStateChange).toBeCalledWith({ + index: 1, + key: '0', + routeNames: ['foo', 'baz', 'qux'], + routes: [{ key: 'foo', name: 'foo' }], + }); +}); + it('throws if navigator is not inside a container', () => { const TestNavigator = (props: any) => { useNavigationBuilder(MockRouter, props); diff --git a/src/types.tsx b/src/types.tsx index 6939980557a350..7a6648ada7ff71 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -54,9 +54,9 @@ export type Router = { /** * Initialize the navigation state. * - * @param routeNames List of valid route names as defined in the screen components. - * @param initialRouteName Route to focus in the state. - * @param initialParamsList Object containing initial params for each route. + * @param options.routeNames List of valid route names as defined in the screen components. + * @param options.initialRouteName Route to focus in the state. + * @param options.initialParamsList Object containing initial params for each route. */ getInitialState(options: { routeNames: string[]; @@ -67,14 +67,31 @@ export type Router = { /** * Rehydrate the full navigation state from a given partial state. * - * @param routeNames List of valid route names as defined in the screen components. - * @param partialState Navigation state to rehydrate from. + * @param options.routeNames List of valid route names as defined in the screen components. + * @param options.partialState Navigation state to rehydrate from. */ getRehydratedState(options: { routeNames: string[]; partialState: NavigationState | PartialState; }): NavigationState; + /** + * 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.initialRouteName Route to focus in the state. + * @param options.initialParamsList Object containing initial params for each route. + */ + getStateForRouteNamesChange( + state: NavigationState, + options: { + routeNames: string[]; + initialRouteName: string; + initialParamsList: ParamListBase; + } + ): NavigationState; + /** * Take the current state and action, and return a new state. * If the action cannot be handled, return `null`. diff --git a/src/useNavigationBuilder.tsx b/src/useNavigationBuilder.tsx index 95165c2dfc07e4..ba8fa81ebb0fa1 100644 --- a/src/useNavigationBuilder.tsx +++ b/src/useNavigationBuilder.tsx @@ -14,6 +14,9 @@ type Options = { children: React.ReactNode; }; +const isArrayEqual = (a: any[], b: any[]) => + a.length === b.length && a.every((it, index) => it === b[index]); + export default function useNavigationBuilder( router: Router, options: Options @@ -78,6 +81,24 @@ export default function useNavigationBuilder( partialState: currentState, }); + if (!isArrayEqual(state.routeNames, routeNames)) { + // When the list of route names change, the router should handle it to remove invalid routes + const nextState = router.getStateForRouteNamesChange(state, { + routeNames, + initialRouteName, + initialParamsList, + }); + + if (state !== nextState) { + // If the state needs to be updated, we'll schedule an update with React + // setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState + // https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops + setState(nextState); + } + + state = nextState; + } + React.useEffect(() => { return () => { // We need to clean up state for this navigator on unmount