Skip to content

Commit b775dba

Browse files
committed
feat: handle route names change
1 parent 95773de commit b775dba

File tree

5 files changed

+110
-6
lines changed

5 files changed

+110
-6
lines changed

example/StackNavigator.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ const StackRouter: Router<CommonAction | Action> = {
8888
return state;
8989
},
9090

91+
getStateForRouteNamesChange(state, { routeNames }) {
92+
return {
93+
...state,
94+
routeNames,
95+
routes: state.routes.filter(route => routeNames.includes(route.name)),
96+
};
97+
},
98+
9199
getStateForAction(state, action) {
92100
switch (action.type) {
93101
case 'PUSH':

example/TabNavigator.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ const TabRouter: Router<Action | CommonAction> = {
7272
return state;
7373
},
7474

75+
getStateForRouteNamesChange(state, { routeNames, initialParamsList }) {
76+
return {
77+
...state,
78+
routeNames,
79+
routes: routeNames.map(
80+
name =>
81+
state.routes.find(r => r.name === name) || {
82+
name,
83+
key: `${name}-${shortid()}`,
84+
params: initialParamsList[name],
85+
}
86+
),
87+
};
88+
},
89+
7590
getStateForAction(state, action) {
7691
switch (action.type) {
7792
case 'JUMP_TO':
@@ -190,4 +205,4 @@ export function TabNavigator(props: Props) {
190205
);
191206
}
192207

193-
export default createNavigator(TabNavigator);
208+
export default createNavigator(TabNavigator);

src/__tests__/index.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ export const MockRouter: Router<{ type: string }> & { key: number } = {
4141
return state;
4242
},
4343

44+
getStateForRouteNamesChange(state, { routeNames }) {
45+
return {
46+
...state,
47+
routeNames,
48+
routes: state.routes.filter(route => routeNames.includes(route.name)),
49+
};
50+
},
51+
4452
getStateForAction(state, action) {
4553
switch (action.type) {
4654
case 'UPDATE':
@@ -535,6 +543,41 @@ it('updates route params with setParams', () => {
535543
});
536544
});
537545

546+
it('handles change in route names', () => {
547+
const TestNavigator = (props: any): any => {
548+
useNavigationBuilder(MockRouter, props);
549+
return null;
550+
};
551+
552+
const onStateChange = jest.fn();
553+
554+
const root = render(
555+
<NavigationContainer onStateChange={onStateChange}>
556+
<TestNavigator initialRouteName="bar">
557+
<Screen name="foo" component={jest.fn()} />
558+
<Screen name="bar" component={jest.fn()} />
559+
</TestNavigator>
560+
</NavigationContainer>
561+
);
562+
563+
root.update(
564+
<NavigationContainer onStateChange={onStateChange}>
565+
<TestNavigator>
566+
<Screen name="foo" component={jest.fn()} />
567+
<Screen name="baz" component={jest.fn()} />
568+
<Screen name="qux" component={jest.fn()} />
569+
</TestNavigator>
570+
</NavigationContainer>
571+
);
572+
573+
expect(onStateChange).toBeCalledWith({
574+
index: 1,
575+
key: '0',
576+
routeNames: ['foo', 'baz', 'qux'],
577+
routes: [{ key: 'foo', name: 'foo' }],
578+
});
579+
});
580+
538581
it('throws if navigator is not inside a container', () => {
539582
const TestNavigator = (props: any) => {
540583
useNavigationBuilder(MockRouter, props);

src/types.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ export type Router<Action extends NavigationAction = CommonAction> = {
5454
/**
5555
* Initialize the navigation state.
5656
*
57-
* @param routeNames List of valid route names as defined in the screen components.
58-
* @param initialRouteName Route to focus in the state.
59-
* @param initialParamsList Object containing initial params for each route.
57+
* @param options.routeNames List of valid route names as defined in the screen components.
58+
* @param options.initialRouteName Route to focus in the state.
59+
* @param options.initialParamsList Object containing initial params for each route.
6060
*/
6161
getInitialState(options: {
6262
routeNames: string[];
@@ -67,14 +67,31 @@ export type Router<Action extends NavigationAction = CommonAction> = {
6767
/**
6868
* Rehydrate the full navigation state from a given partial state.
6969
*
70-
* @param routeNames List of valid route names as defined in the screen components.
71-
* @param partialState Navigation state to rehydrate from.
70+
* @param options.routeNames List of valid route names as defined in the screen components.
71+
* @param options.partialState Navigation state to rehydrate from.
7272
*/
7373
getRehydratedState(options: {
7474
routeNames: string[];
7575
partialState: NavigationState | PartialState;
7676
}): NavigationState;
7777

78+
/**
79+
* Take the current state and updated list of route names, and return a new state.
80+
*
81+
* @param state State object to update.
82+
* @param options.routeNames New list of route names.
83+
* @param options.initialRouteName Route to focus in the state.
84+
* @param options.initialParamsList Object containing initial params for each route.
85+
*/
86+
getStateForRouteNamesChange(
87+
state: NavigationState,
88+
options: {
89+
routeNames: string[];
90+
initialRouteName: string;
91+
initialParamsList: ParamListBase;
92+
}
93+
): NavigationState;
94+
7895
/**
7996
* Take the current state and action, and return a new state.
8097
* If the action cannot be handled, return `null`.

src/useNavigationBuilder.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Options = {
1414
children: React.ReactNode;
1515
};
1616

17+
const isArrayEqual = (a: any[], b: any[]) =>
18+
a.length === b.length && a.every((it, index) => it === b[index]);
19+
1720
export default function useNavigationBuilder(
1821
router: Router<any>,
1922
options: Options
@@ -78,6 +81,24 @@ export default function useNavigationBuilder(
7881
partialState: currentState,
7982
});
8083

84+
if (!isArrayEqual(state.routeNames, routeNames)) {
85+
// When the list of route names change, the router should handle it to remove invalid routes
86+
const nextState = router.getStateForRouteNamesChange(state, {
87+
routeNames,
88+
initialRouteName,
89+
initialParamsList,
90+
});
91+
92+
if (state !== nextState) {
93+
// If the state needs to be updated, we'll schedule an update with React
94+
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
95+
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
96+
setState(nextState);
97+
}
98+
99+
state = nextState;
100+
}
101+
81102
React.useEffect(() => {
82103
return () => {
83104
// We need to clean up state for this navigator on unmount

0 commit comments

Comments
 (0)